Authentication
An Authorization: Bearer sk-echo-... header on every request. The key is
shown only once when you create it: save it. If you lose it, revoke it and create
another.
Restricting an API key to certain IPs
Each API key can be limited to the addresses it makes sense to be used from. If your integration lives on a server with a fixed IP, a stolen key is worth nothing outside it.
It's configured in your account, in the Allowed IPs column of each key. Individual addresses and networks are accepted:
203.0.113.7, 198.51.100.0/24, 192.0.2.10
Left empty, the key works from anywhere, which is the default behavior.
A request from an IP that isn't on the list gets 403 with the code
ip_not_allowed and, in the body, the IP we saw — which is exactly what you need
to add it if you left one out:
{
"error": "ip_not_allowed",
"message": "This API key is restricted to a list of IP addresses and this request does not come from one of them",
"client_ip": "203.0.113.55"
}
403, not a 401. The key is good;
what isn't valid is where it's calling from. Telling them apart matters: a 401
would have you rotating a key that was perfectly fine.The restriction covers every authenticated endpoint, including
/v1/usage/last. Whoever steals a key doesn't always want to spend it;
sometimes seeing how much its owner spends is enough.
client_ip the 403 returns, and add
that one.Analysis in the same request
A transcription can bring voice analysis along without uploading the audio again. They're requested in the query string, comma-separated:
curl https://api.uttera.ai/v1/audio/transcriptions?extras=sentiment,profile,diarize \
-H "Authorization: Bearer $UTTERA_API_KEY" \
-F file=@recording.m4a \
-F model=whisper-1
| Extra | Returns | In the response |
|---|---|---|
sentiment | emotional tone | sentiment |
profile | speaker profile (age, gender) | profile |
diarize | who speaks and when | diarize |
entities | entities in the text: people, places, amounts, identifiers… | entities |
redact | masks the personal data in the text | replaces text |
The first three —sentiment, profile, diarize— run
in parallel over the same audio, so asking for three takes almost as long as asking for
one. entities and redact work on the text already transcribed,
so they run after, are billed per character rather than per second, and aren't affected
by the audio format. With redact, the response's text field arrives
already masked; they're explained in Entities and redaction. Each
analysis is billed separately, and only if it runs: if one fails, the transcription
arrives all the same and the response carries an errors array saying which one was
missing — and that one isn't billed.
sentiment and entities need the Developer plan or
above; redact, Professional or above; profile and
diarize, any plan with audio intelligence. A name that isn't one of the valid ones
returns 400 with the list, not a transcription silently missing its analysis.For sentiment with per-segment or dimensional detail (valence, arousal,
dominance), use the standalone endpoint /v1/audio/sentiment: in
extras it's always the global level.
Formats and languages
Input audio: wav, mp3, flac,
ogg, opus, aiff, m4a and
webm. webm is what a browser records and m4a is what
a phone records, so both work as they come.
webm and m4a are decoded on the node like
the rest, so you don't have to convert them yourself.Output audio (the response_format of speech): wav,
mp3, opus, flac and pcm.
pcm is raw PCM with no header. In the Studio it plays through a dedicated
player (the ▶ Play button, not the normal one); when using it over the API you
have to tell your decoder the four parameters, because the file doesn't carry them:
| Voice | Sample rate | Sample | Channels | Byte order |
|---|---|---|---|---|
| standard | 24,000 Hz | signed 16-bit | 1 (mono) | little-endian |
| HD | 48,000 Hz | signed 16-bit | 1 (mono) | little-endian |
The rate is each voice's native one and is not resampled: HD generates at 48 kHz
and is delivered as is. The only thing that differs between the two is the rate; decoding it
with the wrong one doesn't sound worse, it sounds at double or half speed. With
wav, flac or opus this doesn't apply: the file
declares it itself.
# standard voice
ffmpeg -f s16le -ar 24000 -ac 1 -i voice.pcm voice.wav
# HD voice
ffmpeg -f s16le -ar 48000 -ac 1 -i voice.pcm voice.wav
Any other value of response_format returns 422 with the valid
list.
Size: up to 250 MB per request and 4 hours of audio. An uncompressed 2-hour WAV does not fit: send long audio compressed. The detail is in size and duration.
Transcription: detects the language automatically and covers the ones Whisper supports.
Speech: depends on the engine, and the difference is large. The standard
voice (tts-1) speaks the nine in the table below. The high-quality
voice (tts-1-hd) speaks around 30 — besides those nine, German,
Russian, Polish, Dutch, the Nordic languages, Greek, Turkish, Arabic, Korean and several
Southeast Asian ones, among others — and there's no need to declare the language: it's
inferred from the text. It's explained in
Three voices, not two.
Translation: ~50 text languages. The chain synthesizes with the standard voice, so with speech it's these nine:
| Code | Language | Code | Language |
|---|---|---|---|
en | English | it | Italian |
en-gb | British English | ja | Japanese |
es | Spanish | pt | Portuguese |
fr | French | zh | Chinese |
hi | Hindi |
Voice catalog
The six OpenAI-compatible voices, available with model: tts-1:
alloy · echo · fable · nova ·
onyx · shimmer
The catalog is quite a bit longer, and it is queried over the API rather than copied here, so what you read is what is actually being served right now.
GET /v1/audio/voices
curl https://api.uttera.ai/v1/audio/voices \
-H "Authorization: Bearer $UTTERA_API_KEY"
{
"voices": ["adam", "alex", "alex-pt", "alice", "alloy", …],
"catalog": [
{"name": "adam", "language": "en"},
{"name": "alex", "language": "es"},
{"name": "alex-pt", "language": "pt"},
…
]
}
voices is the bare names, for filling a dropdown;
catalog adds the language of each one. language can be
null for voices that do not belong to one particular language. A voice
that is not on this list returns 422.
Credits
Each service charges for what it consumes. These are the current coefficients:
| Service | Billed by | Credits | One hour of audio |
|---|---|---|---|
| Transcribe | second of input audio | 0.025422 | 91.5 |
| Standard voice | second of generated audio | 0.027626 | 99.5 |
| Cloned voice | second of generated audio | 0.385902 | 1,389.2 |
| Tone | second of audio | 0.005090 | 18.3 |
| Speaker profile | second of audio | 0.003464 | 12.5 |
| Speakers | second of audio | 0.020938 | 75.4 |
| Translation | second of input audio | 0.040000 | 144.0 |
| Summary (LLM) | input token | 0.001605 | — |
| Summary (LLM) | output token | 0.080650 | — |
| Entities & PII redaction | text character | 0.000500 | — |
How each service is composed
| Service | Formula |
|---|---|
| Transcribe | STT |
| Transcribe + tone | STT + tone |
| Speech | TTS over the seconds generated |
| Translate | STT + TTS of the generated audio + translation surcharge |
| Summarize | STT + profile + speakers + model tokens |
A real example
A 40-minute recording (2,424 s) translated into English with speech:
transcription 61.64 2,424.5 s x 0.025422
speech 43.18 2,055.8 s x 0.021004 (English comes out ~15% shorter)
translation 96.98 2,424.5 s x 0.040000
──────
201.80 credits
Plans and limits
| Plan | €/month | Credits | Concurrency | HD voice | Cloning | Summarize | Entities | Redaction | Chapters | SLA |
|---|---|---|---|---|---|---|---|---|---|---|
| Free | 0 | 750 | 1 | — | — | — | — | — | — | — |
| Startup | 19 | 7,500 | 3 | yes | yes | yes | — | — | — | — |
| Developer | 99 | 50,000 | 15 | yes | yes | yes | yes | — | yes | — |
| Professional | 299 | 200,000 | 50 | yes | yes | yes | yes | yes | yes | 95.0 % |
| Business | 999 | 800,000 | 150 | yes | yes | yes | yes | yes | yes | 99.0 % |
| Enterprise | custom | custom | custom | yes | yes | yes | yes | yes | yes | 99.9 % |
Credits renew on your subscription date, not on the 1st. If you move to a smaller plan, you keep the credits already paid for until the period ends.
This same table is served as JSON and without a key, so that a program —or an agent configuring itself— can read the limits before anyone signs up:
curl https://api.uttera.ai/v1/plans
{"plans": {"free": {"price_eur_per_month": 0, "credits_per_month": 500,
"max_concurrent": 1, "rate_limits": {"stt": 1, "tts": 1, "sfx": 0,
"intelligence": 0}, "summary_allowed": false, "voice_clone_allowed": false, …}, …}}
It is the same source that fills the table above: if something changes, both change at once.
Per-second limits
| Plan | Speech | Transcription | Analysis |
|---|---|---|---|
| Free | 1 | 1 | — |
| Startup | 5 | 20 | 5 |
| Developer | 100 | 400 | 50 |
| Professional | 250 | 1,000 | 200 |
| Business | 500 | 2,000 | 500 |
Every response carries X-RateLimit-Remaining-Second and
X-Credits-Remaining-Monthly so you don't have to guess.
Maximum size and duration
| What | Limit |
|---|---|
| Size of the file you send | 250 MB |
| Audio duration | 4 hours |
| Pronunciation: size of the sample | 1 MB — it is a learner reading a sentence, a few seconds of audio |
| Summarise: length of the recording | the same 4 hours — it has no shorter limit of its own |
Text to speech, standard voice (tts-1) | 250,000 characters — roughly four to five hours of speech, depending on how dense the text is |
| Text to speech, standard voice streaming | no length limit |
Text to speech, high-quality and cloned voice (tts-1-hd) | 2,000 characters, about two and a half minutes |
Long recordings: how the summary handles them
A long recording does not fit in the model's context in one go. Rather than summarise part of it, we split it, summarise each piece and then merge — and the merge is not a summary of summaries, which would lose information twice: it is asked to order and combine the pieces, keeping every figure, date and name.
So the summary has no shorter limit of its own: whatever you can transcribe, you can summarise. In an internal test, end to end on a four-hour recording with tone, speaker profile and diarisation: six minutes, complete, with the beginning and the end both present.
If a summary ever comes back cut short, the response says so in truncated. It is
not something you should see, and we would rather tell you than hand you a shortened summary that
reads like a whole one.
Four hours have to fit in 250 MB
The two limits are independent and you have to meet both. For a full four hours, that means staying under 145 kbps:
| Format | What fits in 250 MB |
|---|---|
| mp3 at 64 kbps | 9 h 06 min |
| mp3 at 128 kbps | 4 h 33 min |
| mp3 at 160 kbps | 3 h 38 min |
| mp3 at 256 kbps | 2 h 17 min |
| wav 16-bit, mono, 16 kHz | 2 h 17 min |
| wav 16-bit, stereo, 44.1 kHz | 24 min |
For speech, a 64 kbps mp3 loses nothing that matters to us and leaves you plenty of room. If you send uncompressed wav, expect four hours not to fit.
Text to speech: the limit depends on the voice
There is no single limit, because the two voices are produced by different machinery:
Standard voice (tts-1). It can be requested in two ways:
- Complete response (
/v1/audio/speech): we return the whole audio once it is finished. Because it all has to exist at the same time, the cap is 250,000 characters — roughly four to five hours of speech. We say «roughly» on purpose: how much audio a character becomes depends a lot on the text, and measured on real prose it ranges from 12.6 to 17.5 characters per second. The cap is on the text, not on the duration. - Streaming (
/v1/audio/speech/stream): we send the audio as it is generated. Here there is no length limit, because nothing piles up.
For a long narration, streaming is still the better way. You start hearing the result in seconds instead of waiting for the end, and nothing has to fit in a single response.
High-quality and cloned voice (tts-1-hd). The cap is
2,000 characters per request, about two and a half minutes of speech. That is considerably
less, and it is a limitation of the model behind that voice, not a commercial decision. For
this voice, streaming does not help with long text: use it in chunks of a couple of thousand
characters and chain them.
413 before
generating, instead of incomplete audio. Split the text into shorter chunks and chain them.Errors
| Code | What it means | What to do |
|---|---|---|
400 | The file can't be decoded, or a parameter is invalid. | The reason comes in detail. |
401 | Key missing, malformed or revoked. | Check the Authorization header. |
402 | Credit allowance exhausted. | Wait for the renewal or move up a plan. |
403 | Your plan doesn't include that service. | The message says which plan does. |
413 | File too large. | Split or compress the audio. |
422 | Valid request, impossible to serve. | For example, audio in a language with no voice. Not billed. |
429 | Too many requests per second. | Respect Retry-After. |
502 503 | Temporary failure of a node. | It's retried once automatically. Retry yourself after a few seconds. |
Checking your usage
The charge is computed after the response is sent to you, so it doesn't come inside it. To find out exactly what a request cost you:
GET /v1/usage/last
GET /v1/usage/last?endpoint=/v1/summarize
It returns the credits charged and the breakdown by stage:
{
"endpoint": "/v1/summarize",
"credits": 108.978,
"breakdown": { "stt": 3.1255, "diarize": 2.0029,
"profile": 0.1946, "llm": 103.655 },
"audio_seconds": 95.66,
"input_tokens": 2776, "output_tokens": 1230
}
It keeps the last ten charges for one hour. It's for checking on the spot, not a billing history.
How long each thing takes
Measured numbers, not promises. They vary with the node that serves you and the load at the time:
| Operation | Typical time |
|---|---|
| Transcribing 100 minutes of audio | 10 to 35 s |
| Transcribing a 40-minute recording | under 30 s |
| Standard voice, one sentence | tenths of a second |
| Cloned voice | Considerably more: the sample has to be processed |
| Cache hit | milliseconds |
| Summary of a long recording | The slowest of its stages, not the sum: they run in parallel |
What really needs setting correctly is your client's timeout. The most common failure when integrating is a client with a 30-second default cutting off jobs that were doing fine. It's explained in Integration.
🔴 Synchronous limit: 10 minutes. A synchronous request
(POST /v1/summarize without async) is meant to finish within
10 minutes at most; set your client's timeout to 600 seconds. Almost everything
fits comfortably: transcribing and summarizing a normal recording takes seconds to minutes.
But an intelligence report (?mode=intelligence, the evidence graph) over a
long, dense recording —many claims to anchor— can approach or exceed those 10 minutes,
because its cost grows with how much there is to extract, not with the audio's length.
For those jobs, use async mode (?async=1): you
submit the job, get a job_id immediately, and collect the result with
GET /v1/jobs/<id> when it's ready, without holding a connection that would
eventually be cut. If a synchronous request exceeds 10 minutes, the server cuts it and
returns 504 with {"error":"report_timeout"} and does not
charge you —detect it and resubmit the job in async mode—. In the Studio, the
panel already waits up to those 10 minutes.
Versioning and changes
What you integrate today has to keep working tomorrow. This is the commitment:
For that to mean anything, we have to say what counts as breaking and what doesn't:
| Breaking (six months' notice) | Not breaking (can happen any day) |
|---|---|
| Removing an endpoint or a parameter | Adding a new endpoint |
| Removing or renaming a response field | Adding a field to the response |
| Changing the type or meaning of a field | Adding an optional parameter |
| Changing a parameter's default value | Adding a voice or a language |
| Making something mandatory that was optional | Adding a response header |
| Swapping one error code for a different one | Improving the text of an error message |
From which the practical rule for your code follows: ignore fields you don't recognize instead of failing when you meet them. A client that blows up because the response carries a new key will break on its own, without anybody having broken anything.
Prices are a separate case: they aren't the interface, but they are your invoice. An increase in coefficients is announced with thirty days' notice and is not applied to a cycle already paid for. A decrease applies as soon as it exists.
Service status
If something goes wrong, the first thing is knowing whether it's yours or ours.
GET https://api.uttera.ai/health answers without a key and says whether the
API is up. It's the check you can automate.
curl -s https://api.uttera.ai/health
Every response also carries an X-Request-Id header. Keep it when
something fails: with that identifier we can tell you exactly what happened to your
request, and without it the conversation starts by reconstructing which one it was.
For any incident, write to us with the X-Request-Id, the approximate time
and what you expected to happen.
In the works
Things people ask for, that make sense, and that aren't there yet. We put them here because finding out something doesn't exist after integrating it is worse than knowing now:
| What | What for | Status |
|---|---|---|
| Pronunciation dictionary | Telling the engine how proper names, brands and acronyms are pronounced | To be decided |
| Service accounts and multiple users | So a company can have several people and separate keys under one account | After the beta |
If one of them is blocking you, tell us: what customers ask for is what decides the order.