Text Normalization
How numbers, dates, currency and abbreviations are spoken — and the three layers that control it.
apply_text_normalization follows the ElevenLabs contract — "auto" | "on" | "off", default "auto" — so a migrating integration keeps its payload. "on" verbalises the written forms that have exactly one spoken reading: currency ($1,234.50 → "one thousand two hundred and thirty-four dollars and fifty cents"; also £, €, ¥), ISO dates (2026-08-30 → "August thirtieth, two thousand and twenty-six" — years read as cardinals), times (14:30 → "fourteen thirty", 9:05 → "nine oh five", :00 → "o'clock"), percentages, degrees, digit ordinals (21st → "twenty-first"), bare numbers (decimals digit by digit: 5.25 → "five point two five"), and a short list of one-reading abbreviations (Mr., Mrs., Ms., Prof., e.g., i.e., etc., vs, approx.). Ambiguous forms are NEVER resolved, in any mode: "1/2" is a half, a date or a ratio; "Dr." is Doctor or Drive; "St." is Saint or Street — a wrong guess is worse than the engine's own, so those reach the engine untouched. "auto" currently leaves everything to the engine (modern engines verbalise some of this internally and disagree about which parts, so a second pass on top can fight a better model); "off" guarantees no transform. For terms rather than forms — product names, brand words — use the dictionary layer below. For one specific span, use SSML.
Authentication
Synthesis flags ride the normal tts:synthesize scope. Managing saved pronunciation dictionaries uses the voice scopes — voices:read to list, voices:write to create and edit — so any key minted for voice management can already use them.
Create a key in Settings → API Keys. It is shown once, so copy it then. Every example below reads it from $SONICVOX_API_KEY.
Endpoints
One flag, three positions — and a billing guarantee worth knowing: normalization changes what is SPOKEN, not what is billed or recorded. You are charged on the characters you sent (verbalising "$1,234.50" into fifty-odd characters of words does not turn a 9-character request into a 55-character charge), and moderation, history and the idempotency fingerprint all see your original text. with_timestamps aligns against what was spoken, so the timings line up with the verbalised words. SSML is the surgical alternative (second tab): set "ssml": true and mark up spans by hand — <say-as> for characters/digits/ordinal/date/telephone readings, <sub alias="…"> to substitute, <break time="750ms"/> for real timed silence (the bracket forms [pause], [pause:750ms], [pause:1.5s] convert to breaks too, clamped 50 ms–10 s), plus <prosody> and <emphasis>. <phoneme> is parsed but NOT applied — no deployed engine consumes phonetic input, so the grapheme text is spoken; use a respelling instead. SSML is buffered-only: the streaming endpoint takes plain text.
- Scope
- tts:synthesize
- Credits
- ⌈characters sent / 100⌉ × 100 — the verbalised expansion is never billed
# "on": the price is read as money, the date as a date.
curl https://staging.sonicvox.ai/api/v1/text-to-speech \
-H "sv-api-key: $SONICVOX_API_KEY" \
-H "content-type: application/json" \
-d '{
"text": "Launch price $1,234.50, shipping 2026-08-30 at 14:30.",
"voice_id": "cmpwrfi46015zkww3t8nui4ye",
"apply_text_normalization": "on"
}' \
--output launch.wavHTTP/1.1 200 OK
content-type: audio/wav
x-credits-charged: 100
<binary audio — "one thousand two hundred and thirty-four dollars
and fifty cents … August thirtieth, two thousand and twenty-six
at fourteen thirty">Create a saved dictionary of respellings. Its alias entries apply automatically to every synthesis from your account — and, marked as the workspace glossary, to everyone in the workspace — merged UNDER any per-request lexicon ({ grapheme: respelling }, up to 200 entries, whole-word), which always wins; use_pronunciation_dictionaries: false skips the saved entries for one request. Alphabet honesty: only "alias" entries (plain respellings) are applied at synthesis. ipa, cmu-arpabet and x-sampa are accepted and stored but INERT — no deployed engine consumes phonetic alphabets, and this page says so rather than letting you ship a dictionary of dead phonemes.
- Scope
- voices:write
- Credits
- 0
curl https://staging.sonicvox.ai/api/v1/pronunciation-dictionaries \
-X POST \
-H "sv-api-key: $SONICVOX_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Brand terms",
"language": "en-US",
"entries": [
{ "grapheme": "SonicVox", "phoneme": "sonic vox", "alphabet": "alias" }
]
}'HTTP/1.1 201 Created
{
"dictionary": {
"id": "cmf1x0000000000000000000",
"name": "Brand terms",
"description": null,
"language": "en-US",
"is_workspace_default": false,
"entry_count": 1,
"created_at": "2026-08-30T00:00:00.000Z",
"updated_at": "2026-08-30T00:00:00.000Z"
}
}When it fails
Every error carries type, code, message, request_id and a doc_url. Branch on type for retry policy.
| validation_error | apply_text_normalization outside "auto" | "on" | "off", a lexicon over 200 entries, a grapheme over 80 characters, or a respelling over 120. |
| insufficient_scope | Dictionary management needs voices:write; the key's scopes are listed in the message. |