UtteraUttera

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.

Don't put it in the browser. The key gives access to your credit allowance. Call the API from your server, never from code the end user can read.

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"
}
It's a 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.

Before restricting a key in production, check which IP you actually come out of. It isn't always the one you think: behind NAT, a load balancer, or an internet connection with several lines, your traffic can appear from different addresses. Fire a request, look at the 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
ExtraReturnsIn the response
sentimentemotional tonesentiment
profilespeaker profile (age, gender)profile
diarizewho speaks and whendiarize

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:

VoiceSample rateSampleChannelsByte order
standard24,000 Hzsigned 16-bit1 (mono)little-endian
HD48,000 Hzsigned 16-bit1 (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:

CodeLanguageCodeLanguage
enEnglishitItalian
en-gbBritish EnglishjaJapanese
esSpanishptPortuguese
frFrenchzhChinese
hiHindi

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:

ServiceBilled byCreditsOne hour of audio
Transcribesecond of input audio0.032673117.6
Standard voicesecond of generated audio0.02100475.6
Cloned voicesecond of generated audio0.4067481,464.3
Tonesecond of audio0.00509018.3
Speaker profilesecond of audio0.00346412.5
Speakerssecond of audio0.02093875.4
Translationsecond of input audio0.080000288.0
Summary (LLM)input token0.001605
Summary (LLM)output token0.080650
An output token costs 54 times an input one. Generating is far more expensive than reading, which is why the model's share dominates a summary's bill even when the transcript is long.

How each service is composed

ServiceFormula
TranscribeSTT
Transcribe + toneSTT + tone
SpeechTTS over the seconds generated
TranslateSTT + TTS of the generated audio + translation surcharge
SummarizeSTT + 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
What's billed is the audio generated, not the audio you upload. When translating, the target language almost never lasts the same as the source.

Plans and limits

Plan€/monthCreditsConcurrencyHD voiceCloningSummarizeSLA
Free05001
Startup197,5003yesyesyes
Developer9950,00015yesyesyes
Professional299200,00050yesyesyes95.0 %
Business999800,000150yesyesyes99.0 %
Enterprisecustomcustomcustomyesyesyes99.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

PlanSpeechTranscriptionAnalysis
Free11
Startup5205
Developer10040050
Professional2501,000200
Business5002,000500

Every response carries X-RateLimit-Remaining-Second and X-Credits-Remaining-Monthly so you don't have to guess.

Errors

CodeWhat it meansWhat to do
400The file can't be decoded, or a parameter is invalid.The reason comes in detail.
401Key missing, malformed or revoked.Check the Authorization header.
402Credit allowance exhausted.Wait for the renewal or move up a plan.
403Your plan doesn't include that service.The message says which plan does.
413File too large.Split or compress the audio.
422Valid request, impossible to serve.For example, audio in a language with no voice. Not billed.
429Too many requests per second.Respect Retry-After.
502 503Temporary failure of a node.It's retried once automatically. Retry yourself after a few seconds.
If a stage fails, it isn't billed. In multi-step chains — translate, summarize — a failure in the middle returns the error without charging credits, and a partial failure returns what could be done with a warning, billing only that part.

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:

OperationTypical time
Transcribing 100 minutes of audio10 to 35 s
Transcribing a 40-minute recordingunder 30 s
Standard voice, one sentencetenths of a second
Cloned voiceConsiderably more: the sample has to be processed
Cache hitmilliseconds
Summary of a long recordingThe 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:

Six months' notice for any breaking change. If one day we have to change something incompatibly, the previous version keeps working for at least six months from the announcement, and we notify you by email at your account address.

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 parameterAdding a new endpoint
Removing or renaming a response fieldAdding a field to the response
Changing the type or meaning of a fieldAdding an optional parameter
Changing a parameter's default valueAdding a voice or a language
Making something mandatory that was optionalAdding a response header
Swapping one error code for a different oneImproving 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.

And there is a public status page: uttera.ai/en/status. It checks the API live and carries the incident history, with the duration and cause of each one. Every interruption that affected customer requests is published, including the short ones: a history that only shows the big outages is no use for judging a provider.

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:

WhatWhat forStatus
WebhooksSo we notify you when a long job finishes, instead of holding the connection openTo be decided
Pronunciation dictionaryTelling the engine how proper names, brands and acronyms are pronouncedTo be decided
Service accounts and multiple usersSo a company can have several people and separate keys under one accountAfter the beta

If one of them is blocking you, tell us: what customers ask for is what decides the order.