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 a key to certain IPs
Each 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 |
All three run in parallel over the same audio, so asking for three takes almost as
long as asking for one. Each is billed separately, and only if it runs: if one
analysis 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 needs the Developer plan or above;
profile and diarize, any plan with audio intelligence. A name that
isn't one of the three returns 400 with the valid 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 — the same ones for transcribing and for analyzing. webm is
what a browser records and m4a is what a phone records, so both work as they
come.
Output audio (the response_format of speech): wav,
mp3, opus, flac and pcm.
pcm is raw PCM with no header, so the web app doesn't offer it — a browser
can't play it — and 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 400 MB per request and 3 hours of audio, with a 100 MB file cap on transcription. A 3-hour WAV weighs about 346 MB and doesn't fit under that cap: for long audio, send it compressed.
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
Standard voices available with model: tts-1:
alloy · echo · fable · nova ·
onyx · shimmer
The extended catalog is under review; it will be published once it's settled.
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.032673 | 117.6 |
| Standard voice | second of generated audio | 0.021004 | 75.6 |
| Cloned voice | second of generated audio | 0.406748 | 1,464.3 |
| 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.080000 | 288.0 |
| Summary (LLM) | input token | 0.001605 | — |
| Summary (LLM) | output token | 0.080650 | — |
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 79.22 2,424.5 s x 0.032673
speech 43.18 2,055.8 s x 0.021004 (English comes out ~15% shorter)
translation 193.96 2,424.5 s x 0.080000
──────
316.36 credits
Plans and limits
| Plan | €/month | Credits | Concurrency | HD voice | Cloning | Summarize | SLA |
|---|---|---|---|---|---|---|---|
| Free | 0 | 500 | 1 | — | — | — | — |
| Startup | 19 | 7,500 | 3 | yes | yes | yes | — |
| Developer | 99 | 50,000 | 15 | yes | yes | yes | — |
| Professional | 299 | 200,000 | 50 | yes | yes | yes | 95.0 % |
| Business | 999 | 800,000 | 150 | yes | yes | yes | 99.0 % |
| Enterprise | custom | custom | custom | 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.
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.
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 server holds the connection for up to 7200 seconds, and 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.
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 |
|---|---|---|
| Webhooks | So we notify you when a long job finishes, instead of holding the connection open | To be decided |
| 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.