At a glance
Authorization: Bearer and the base https://api.uttera.ai/v1; each response carries its cost breakdown. New here? Start with the Quickstart.Jump to: Security · Transcribe (STT) · Speech (TTS) · Streaming · Translate · Analyse the voice · Summarize
Security
Transcripts, translations and summaries are generated from audio we do not control: your customer produces it or it comes from them. Anyone can try to slip sentences shaped like commands into a recording so that they get executed in your system.
We harden the summary against that kind of manipulation, but no defense is complete. If you're going to pass our output to an agent, a CRM or any automation with permissions, treat it as untrusted text: don't execute it, don't read it as orders, and validate it before acting on it.
And what a speaker claims in a recording is not a proven fact, even if it ends up in the summary.
Transcribe
POST /v1/audio/transcriptions
Turns a recording into text. Detects the language on its own.
You can use this service directly, without writing code, on the Studio page: the Transcribe audio card.
| Parameter | Type | Description |
|---|---|---|
file | file | Required. The audio. |
model | text | whisper-1 |
language | text | ISO code. If omitted, it's detected. |
prompt | text | Context to help with proper names or jargon. |
response_format | text | json · text · verbose_json · srt · vtt |
extras | text (query) | sentiment · profile · diarize, comma-separated |
temperature | number | 0 to 1. Defaults to 0. |
extras | query | ?extras=sentiment adds tone analysis in the same request. |
The response carries the X-Audio-Duration header with the exact seconds that
were billed.
Text to speech
POST /v1/audio/speech
You can use this service directly, without writing code, on the
Studio page: the Text to digital voice
(tts-1) and Text to cloned voice (tts-1-hd) cards.
| Parameter | Type | Description |
|---|---|---|
input | text | Required. What needs to be said. |
model | text | tts-1 standard voice · tts-1-hd high-quality voice. See the three voices. |
voice | text | Name from the catalog. See voices. |
response_format | text | mp3 · wav · opus · flac · pcm |
speed | number | Speed. 1.0 is normal. |
language | text | Reading language. If omitted, the voice sets it. See how it is chosen. |
cache | boolean | false keeps this request out of the cache: nothing is read and nothing is written. See below. |
curl -X POST https://api.uttera.ai/v1/audio/speech \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"Your order ships tomorrow.","voice":"nova","response_format":"mp3"}' \
--output voice.mp3
gustaria is stressed
gus-ta-ria; gustaría is stressed gus-ta-rí-a.
We do not add them for you, on purpose: esta and está, papa and papá, termino and terminó are different words. An automatic corrector would get many right and, when it got one wrong, it would be changing what you asked us to say. We would rather read your text as written.
Which language is read, and who decides
The language does not set the timbre: it picks the rules by which text becomes sound. Getting it wrong does not sound “accented”, it sounds broken. A Spanish text read with English rules comes out literally like this:
Mi Gasteria reservar una mesa para dues personas
So it is resolved in three steps, and you rarely have to think about it:
| Order | Where it comes from |
|---|---|
| 1 | The language you send. Always wins. |
| 2 | The voice's language. Every catalogue voice speaks its own,
and /v1/audio/voices publishes it. Ask for
alloy and it reads English; ask for dora and it reads Spanish. |
| 3 | The server default, English. It only reaches voices with no language of their own: cloned and multilingual ones, which speak whatever they are asked to. |
/v1/audio/speech has no language field — it is our extension — so
your code does not send it, and step 2 does the right thing: the voice you pick decides the
language.When it is worth sending: with a cloned or multilingual voice (they have no language of their own), or when you deliberately want a voice to read a language other than its own.
The voice sample: what to upload
Cloning a voice needs a reference sample. What works best:
| Recommended | Why |
|---|---|
| 6 to 12 seconds | Below 6 you lose tonal depth. Above 12 it doesn't improve proportionally and only adds latency. |
| A complete sentence, with natural intonation | The model captures how that voice rises and falls, and it needs to see it happen. |
| No background noise | This is what most people get wrong. Music, room echo or air conditioning contaminate the output voice and will show up in everything you generate afterwards. |
On whose voice you may clone — which is the question that causes the most trouble — see the legal framework: a voice is protected, and public material is not an authorization.
Three voices, not two
The same route gives three different things, and what decides which is what you send:
| What you send | What you get | Plan |
|---|---|---|
tts-1, or nothing | Standard voice. Our own catalog, fast and cheap. | All |
tts-1-hd | High-quality voice. The same catalog, premium engine. | Paid |
tts-1-hd + a voice sample | Cloning. Your voice, on the premium engine. | Paid |
The sample is sent as custom_voice_file in a multipart form; without
it, the request goes as JSON. That's the whole difference between asking for a catalog voice
in high quality and cloning one.
tts-1 is the
sensible option.They don't speak the same languages
It's the difference that surprises people most, and it isn't in the timbre:
| Languages | Catalog voices | |
|---|---|---|
tts-1 · standard | 9: Spanish, English (and British), French, Italian, Portuguese, Hindi, Japanese and Chinese | Many, and each tied to its language |
tts-1-hd · high quality | Around 30: besides the above, German, Russian, Polish, Dutch, the Nordic languages, Greek, Turkish, Arabic, Korean and several Southeast Asian ones, among others | Few, but each speaks all 30 |
The reason is that they are engines of a different nature. The standard one has a fixed catalog of voices, each trained for its language. The high-quality one starts from a sample — which is why it can clone — and that same voice reads any of the languages it knows.
With tts-1-hd there's no need to declare the language: it's inferred
from the text. With tts-1 it does matter, because the chosen voice is what
fixes it.
Line breaks cost money
Speech is billed per second generated, not per character. And a line break makes the engine insert a pause. So the same text costs differently depending on how it's formatted, and it's worth knowing before the invoice surprises you.
Measured with the same sentence repeated eight times, changing only what's in between:
| Between sentences | Duration | Versus a space |
|---|---|---|
| space · 2 spaces · 4 spaces | 12.2 s | — |
| comma · semicolon · colon · ellipsis · dash | 12.1–12.3 s | — |
| line break | 21.4 s | +75% |
| 2 breaks · 3 breaks | 21.4 s | +75% |
Spaces do nothing. Neither two nor four. Nor does punctuation: a comma, a period or a dash sound the same as a space as far as the clock is concerned.
Only the line break creates a pause, and each one costs 1.31 s (0.028 credits).
More breaks don't lengthen the pause. One, two or three are exactly the same. They are no use for asking for a longer wait.
On a real text the difference isn't small. La canción del pirata, with its ~96 lines, spends 126 seconds on pauses alone: a little over half of what synthesizing the whole thing costs.
If you don't want the pauses, strip the line breaks before sending the text: the same words as one running paragraph cost nearly half. And if you do want them, now you know what they're worth.
Why the chain gives a different number
If you transcribe an audio file and then translate it, you'll see that the speech for the translation costs considerably less than the original synthesis of the same text. It isn't a billing error: recognition returns one running paragraph, without the original's line breaks. That flat text is synthesized without pauses, and so it lasts — and costs — less.
Put another way: line breaks don't survive the trip through audio. If keeping them matters to you, hold on to the source text; they can't be recovered from the transcript.
X-Cache: HIT header.
The cache is per node. Since requests are spread across several, the first repetitions of a text may not hit: each node fills it the first time its turn comes. After that it hits.
Turning off the cache, request by request
Some work can't have the audio sitting on someone else's disk even for an hour: medical dictation, legal notes, a personal message. You can turn the cache off yourself, on each request, without asking us for anything and without changing your account. The audio is generated and delivered to you all the same; what doesn't happen is anything being written to or read from disk.
Three equivalent ways, whichever fits you best:
# 1) In the JSON body
-d '{"model":"tts-1","input":"Private notes","voice":"nova","cache":false}'
# 2) As a form field (accepts 0 / false / no / off)
-F input="Private notes" -F voice=nova -F cache=false
# 3) With the plain old HTTP header, without touching the body
-H "Cache-Control: no-cache"
The response always tells you what was done, so you don't have to take our word for it:
X-Cache | What happened |
|---|---|
HIT | Served from cache. Costs 10%. |
MISS | Generated and stored for the next hour. |
BYPASS | You asked without cache: generated and nothing stored. |
ADHOC | Voice cloned on the fly. Never cached, whether you ask or not. |
DISABLED | The cache is switched off on the server. |
Streaming: hearing it as it's generated
POST /v1/audio/speech/stream
The normal speech endpoint gives you the file when it's complete. This one hands it to you as it's generated, in chunks, so the first sound comes out almost immediately instead of waiting for the last word.
If you're building a phone system, an assistant that answers, or anything where a person is waiting on the other end, this is the difference between a conversation and a queue.
/v1/audio/speech | /v1/audio/speech/stream | |
|---|---|---|
| Returns | The complete file | Chunked audio/wav (Transfer-Encoding: chunked) |
| Formats | mp3 · wav · opus · flac · pcm | wav only |
| Cache | Yes, one hour at 10% | No: there's no file to store |
| Cloned voice | Yes | Yes |
| Price | The same: per second of audio generated | |
curl -N -X POST https://api.uttera.ai/v1/audio/speech/stream \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"Your order ships tomorrow.","voice":"nova","language":"en"}' \
--output - | aplay
curl's -N matters: without it, curl buffers the response and you lose
exactly what you came for.
Translate
The 50 languages you can write in: these are what translation accepts as INPUT.
sqAlbanianarArabicazAzerbaijanieuBasquebnBengalibgBulgariancaCatalanzh-HansChinese (Simplified)zh-HantChinese (Traditional)csCzechdaDanishnlDutchenEnglisheoEsperantoetEstonianfiFinnishfrFrenchglGaliciandeGermanelGreekheHebrewhiHindihuHungarianidIndonesiangaIrishitItalianjaJapanesekoKoreankyKyrgyzlvLatvianltLithuanianmsMalaynbNorwegian BokmålfaPersianplPolishptPortuguesept-BRPortuguese (Brazil)roRomanianruRussianskSlovakslSlovenianesSpanishswSwahilisvSwedishtlTagalogthThaitrTurkishukUkrainianurUrduviVietnamese
And the 9 that can come back SPOKEN: for the rest, translation comes back as text. If you ask for audio in one without a voice, the request is rejected with 422 before spending anything.
zhChineseenEnglishen-gbEnglish (British)frFrenchhiHindiitItalianjaJapaneseptPortugueseesSpanish
POST /v1/translate
The full chain: it transcribes the audio, translates the text and synthesizes it in the target language. It accepts audio or text input, but not text to text: if text goes in, the output has to include audio.
You can use this service directly, without writing code, on the Studio page: the Translate a recording card.
| Parameter | Type | Description |
|---|---|---|
file | file | The source audio. |
target | query | Required. Target language. |
source | query | Source language. Detected by default. |
response | query | both text and audio · text text only · audio audio only |
voice | query | Output voice, from the standard catalog. Ignored when you ask for preserve_voice. |
preserve_voice | query | 1 to transpose: the translation is spoken in the original speaker's voice, cloned from that same recording. Requires audio input and a paid plan. |
speed | query | Reading speed of the returned audio. 1.0 is normal; 0.25 to 4.0. Since speech is billed per second generated, it also changes the price. |
format | query | Format of the returned audio: mp3 (default) · wav · opus · flac · pcm. It's synthesized with the standard voice, so pcm comes out at 24,000 Hz. |
curl -X POST "https://api.uttera.ai/v1/translate?target=en&response=both" \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-F file=@recording.mp3
It returns source_text, the translated text, and
audio in base64 with its audio_format. Also
source_language: if you did not declare source, that field carries
the language we detected, not the word auto. It is worth reading.
source.
It is always more reliable than leaving it to detection.422 before spending anything and returns the valid list. Use
response=text for the rest.
It's a limitation of the engine this chain uses, not of the product: the high-quality voice speaks around 30 languages. If you need audio in one of the others, tell us.
Transposing the voice
With preserve_voice=1 the translation is not read in one of our catalogue
voices: it is read in the speaker's own voice. The same recording does both jobs,
transcription and cloning, so there is nothing else to upload.
curl -X POST "https://api.uttera.ai/v1/translate?target=en&response=both&preserve_voice=1" \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-F file=@recording.mp3
The response carries preserved_voice: true and voice: "original".
Check it: that is how you know, without listening to the audio, which of the two products
you were served — because they do not cost the same.
Inside, the full flow is three stages on a single request of yours:
| Stage | What happens | Billed |
|---|---|---|
| 1. Transcribe | Your recording goes through speech recognition. | Per second of input audio. |
| 2. Translate | The text is translated into the target language. | Included in the chain surcharge. |
| 3. Speak | The translated text is synthesized cloning the voice of that same recording. | Per second of generated audio, at the cloned voice rate. |
/v1/audio/speech. The
Studio page shows you the estimated cost
before you run it, with the box already ticked.preserve_voice is rejected with
422: there is no voice to transpose. And if your plan does not include
cloning, with 403 and the name of the parameter you need to drop.
The cloned voice is not stored. It is derived in memory for that one request and goes away with it, exactly as in adhoc cloning.
Analyze the voice
Three endpoints over the same audio, each with its own price.
You can use these analyses directly, without writing code, on the Studio page: they are checkboxes on the Transcribe audio card, ticked over the same audio.
| Endpoint | What it does |
|---|---|
POST /v1/audio/sentiment | Emotional tone of the recording. |
POST /v1/audio/profile | Speaker profile: estimated age range and gender. |
POST /v1/audio/diarize | Who speaks and when, with timestamps per speaker. |
curl -X POST https://api.uttera.ai/v1/audio/diarize \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-F file=@recording.mp3
Entities and personal-data redaction
Two operations over the text of a transcription, not over the audio. That's why they run after transcribing, not in parallel with the voice analysis.
| Operation | What it does | Plan |
|---|---|---|
Entities · entities | Detects and classifies what appears in the text: people, organizations, places, dates, amounts, emails, phone numbers, identifiers. | Developer or above |
Redaction · redact | Returns the transcription with personal data masked. It detects a name once and covers it in every occurrence. | Professional or above |
Entities come from a model that works on the language, not from a word list. Redaction goes one step further: on top of what the model flags, it adds validators that check the shape of each datum before covering it —the check digit of a Spanish DNI/NIE, the mod-97 of an IBAN, the Luhn algorithm of a card number—, so it won't mask a number that only looks like a card, nor let through one that is.
They're requested in two ways: as an extra on a transcription
(?extras=entities,redact) or inside a summary. They're
billed per character of text processed; the detail is in the
coefficient table.
redact, the text is masked before it reaches the model that summarizes: the
summary, the transcription we return, and the PDF report all inherit the redacted text. And with
redact the response does not include the entity list —returning the real
names would leak exactly what you paid to hide—; if you want the list, ask for
entities.Summarize a recording
POST /v1/summarize Startup and above
It transcribes, analyzes tone, profiles the speaker, separates speakers, and out of all that generates a structured summary. The four source services run in parallel, so the wait is that of the slowest, not the sum.
You can use this service directly, without writing code, on the Studio page: it's one more checkbox on the Transcribe audio card.
| Parameter | Type | Description |
|---|---|---|
file | file | Required. The recording. |
exclude | query | Disables enrichments: ?exclude=emotion,profile,diarize |
language | query | Language of the summary. Spanish by default. |
entities | query | With ?entities=1 adds the entity list. ?report=pdf forces it. Developer and above |
redact | query | With ?redact=1 redacts PII before summarizing. Professional and above |
chapters | query | With ?chapters=1 adds time-stamped chapters. Requires response_format=verbose_json. Developer and above |
mode | query | With ?mode=intelligence it turns on intelligence analysis: it classifies, structures and extracts the evidence graph. Professional and above |
report_schema | query | Forces the report type (acta, comercial, legal, entrevista, incidente… or your own) instead of letting the classifier choose. |
report_prompt | query | Adjusts the tone or framing of the report in plain language. |
report | query | With ?report=pdf it also returns the report as a signed PDF (in report.pdf_base64), which a third party can verify. |
async | query | With ?async=1 the job is processed in the background: the request returns 202 with a job_id and you collect it later. For the long analyses. See Asynchronous mode. |
webhook | query | Only with async=1: an https URL we deliver the result to, signed, once it's ready. Optional; without it you collect the result yourself with GET /v1/jobs/<id>. |
It returns summary, transcript, enrichment and a
usage block with the credits broken down by stage —with an ner
line if you asked for entities or redaction—. Depending on what you ask for, it also brings
entities, the redacted flag, or the chapters array.
usage includes reasoning_tokens: the part of
output_tokens that was reasoning rather than summary text. It is
included in the charge —as with the reasoning APIs from OpenAI or
Anthropic—, but we break it out so you can see what fraction of your output was
thinking and don't assume you pay it all for the visible summary: that is
output_tokens − reasoning_tokens. It is an estimate of the split;
the amount billed is output_tokens.warnings array, and that stage
isn't billed.Chapters
With ?chapters=1 the summary also brings a chapters array. Each
chapter carries start (the second it begins at), title and
summary —so you can jump through a long recording without listening to all of
it—.
They're computed in the same pass as the summary: it's not a second call to the model nor is the transcription re-sent, so their cost is a few more output tokens, with no separate coefficient.
verbose_json —add response_format=verbose_json to the request—: that's
where each chapter's seconds come from. If they're missing, the summary comes out anyway and
warnings flags it with chapters_unavailable.Verifiable report
With ?mode=intelligence the summary becomes a checkable report: it
classifies the recording, structures it into sections and extracts an evidence graph that
anchors each statement to its quote, its speaker and its minute; with ?report=pdf it
comes as a signed PDF. It is Uttera's distinctive service and is explained in full —what it is,
how anchoring works, parameters and examples— in its own chapter:
Verifiable report.
Asynchronous mode
The deepest audio analyses take longer than a single HTTP connection can hold. That is what
asynchronous mode is for: you add ?async=1 to your
/v1/summarize request and, instead of waiting, you get a job_id
straight away. The job runs in the background and you collect the result by polling, or we
deliver it to a webhook. It works with every option you already know
(mode=intelligence, report=pdf, redact,
language, chapters…): they are re-applied as-is when processing.
1. Submit the job. Just like a normal summary, but with async=1:
curl -X POST "https://api.uttera.ai/v1/summarize?mode=intelligence&report=pdf&async=1" \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-F "file=@meeting.mp3"
It answers 202 Accepted:
{ "job_id": "9e7220186fa76062295e767ea856e9b7",
"status": "queued",
"poll": "/v1/jobs/9e7220186fa76062295e767ea856e9b7" }
2. Collect it. Poll GET /v1/jobs/<job_id> with the same key:
curl "https://api.uttera.ai/v1/jobs/9e7220186fa76062295e767ea856e9b7" \
-H "Authorization: Bearer $UTTERA_API_KEY"
| While it works | {"job_id":"…","status":"queued"} or "running". Poll again in a few seconds. |
|---|---|
| When it finishes | Returns the full result —the same body the synchronous call would have given you (summary, transcript, usage, and depending on what you asked for evidence_graph, report, redacted…). |
| If it failed | {"status":"error","error":"…"}. |
GET
hands you the result successfully, we delete it. A second GET of the same
job_id returns 410 Gone. Store it yourself when you receive it.Webhook (optional). If in step 1 you add &webhook=https://your-server/uttera,
as soon as the job finishes we POST the result to that URL in the body. We retry
at 1 min, 5 min, 30 min, 2 h and 6 h; if they run out, the result still waits for you
on the GET until the 24 hours. A successful delivery (a 2xx response)
counts as a collection and deletes the result.
Every webhook POST is signed so you can check it is ours:
X-Uttera-Job | the job_id. |
X-Uttera-Signature | sha256=<signature>, an HMAC-SHA256 of the exact body. |
The signing key is derived from your API key (it never travels): it is
HMAC-SHA256(your_api_key, "uttera-webhook-v1") in hex. To verify: compute that
key, take HMAC-SHA256(key, received_body) and compare it with the header
signature.
# verify the webhook (Python)
import hmac, hashlib
secret = hmac.new(API_KEY.encode(), b"uttera-webhook-v1", hashlib.sha256).hexdigest()
expected = "sha256=" + hmac.new(bytes.fromhex(secret), body_bytes, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, header_X_Uttera_Signature)
POST …?async=1 returns 429 until one
finishes. Billing is the same as synchronous: you are charged when the job is processed, with
its usage broken down in the result.Saving tokens on your LLM
This is the use of the summary that fewest people see and that saves the most money. If what you want is for a language model from another provider — Claude, GPT, Gemini, whichever — to work on what was said in a call, the expensive route is sending it the whole transcript. The cheap one is sending it the summary.
Measured on a real 70-minute recording of varied prose, counting with the
o200k_base tokenizer:
| What you send the LLM | Words | Tokens |
|---|---|---|
| Full transcript | 10,429 | 15,410 |
| Structured summary | 294 – 422 | 484 – 673 |
It's given as a range because the summary is not deterministic: the same recording, run twice, gave 484 and 673 tokens. Between 20 and 30 times fewer input tokens, and the saving grows with duration: the transcript grows in a straight line with the minutes of recording, and the summary doesn't — it stays at a few hundred tokens. On a short recording it makes no difference; on an archive of calls it's the difference between an LLM invoice you can pay and one you can't.
The response brings summary and transcript in the same
call, so you don't have to choose in advance or pay twice: you decide in your code which
of the two goes up to the LLM.
There's a complete, runnable example — summarize here, send only the summary to the LLM and count the tokens you saved — in uttera-examples/llm-tokens.