A caller asks which card the agent is about to charge. The agent says "the card ending in four thousand three hundred and twenty-one." Technically correct, and nobody talks like that. The caller hesitates, asks again, and your two-second exchange just became a ten-second one.
Entities are where voice agents break: phone numbers, prices, dates, email addresses, street addresses, codes and names. They're also the exact details a caller has to hear right. This guide goes entity by entity. For each, you'll see how it fails, what AssemblyAI's Text-to-Speech API already handles on the server, what to write when you need a specific reading, and how to get your LLM to write it that way from the start.
Why entities break text-to-speech
Most entity errors come from one of three places:
- The LLM writes an ambiguous form.
4321can be a quantity, a year-like number or four separate digits.1/2can be a date or a fraction. The text alone doesn't say which one you meant. - The TTS picks a reading. Every TTS model has to decide how to say
St,Dr.or10/24/2026. When the form is rare, it can guess wrong. - The written form isn't the spoken form. People don't say the year when confirming next Tuesday's appointment, and they don't read a card number as one big number.
AssemblyAI-TTS-1 handles the second problem for the common cases before the model ever sees your text. Its server writes currencies, phone numbers, card and ID numbers, long numbers after a label like "order number", email and web addresses, measurements, dates with years, weekday abbreviations and US street addresses out in words, in every supported language. In English it also writes out shorthand like 3rd Fl, symbols like ±, and press * as "press star." The full list is in the TTS prompting guide.
What it doesn't have is SSML or a pronunciation dictionary. The TTS connection docs state there's no pronunciation dictionary or SSML support on either the WebSocket or the HTTP API. You control readings with the text itself, which is also what your LLM is good at producing.
Phone numbers, account IDs and codes
The failure: a long number read as a quantity ("four million, three hundred...") or digits run together with no grouping, so the caller can't write them down.
What the server does: phone numbers are normalized into digit groups. (415) 555-2876 is read as "four one five, five five five, two eight seven six." Card numbers, ID numbers and long numbers after a label such as order number are also written out before synthesis.
What to write when you need a specific reading:
- Digit by digit: separate the digits with dashes.
4-3-2-1is read as "four three two one." - Digit by digit, in groups: put a dash between groups.
4321-5678-9012-3456is read digit by digit with a short pause after each group of four. - As a whole number: use commas.
4,321is read as "four thousand three hundred and twenty one." - Letters in a code: put dashes between the letters, the same control the docs give for uncommon acronyms. For a mixed code like
BX7Q, writeB-X-7-Qand test it on your voice.
This is the entity type where TTS models differ most. On Cekura's independent TTS benchmark, AssemblyAI-TTS-1 ranks #1 on phone numbers and codes at 91.7%, across 104 agent sentences and 27 models.
Money, dates and times
The failure: $1,248.50 read as "one thousand two hundred forty-eight point five zero dollars," or 11/3/2026 read with the full year when the caller only needs "November 3rd."
What the server does: a currency symbol next to a number makes it an amount. $12.13 is read as "twelve dollars and thirteen cents," and 12,50 € is read as an amount too. Dates written in numbers with a year are normalized, so 10/24/2026 becomes "October twenty-fourth, twenty twenty-six." Times like 4:30 PM are read as "four thirty p m."
What to write: write dates the way you'd say them on a call. The prompting guide's own before-and-after swaps Tuesday, 11/3/2026 for Tuesday, November 3rd, dropping the year a person wouldn't say. Keep the currency symbol on every amount, and keep commas in large amounts so they're read as whole numbers.
Emails and URLs
The failure: an email read as one long word, or a username the caller can't spell back.
What the server does: email and web addresses are normalized. support@company.com is read as "support at company dot com," and example.com/help as "example dot com slash help."
What to write: that default works for dictionary-word usernames. For anything a caller has to type, spell the part before the @ with dashes: S-U-P-P-O-R-T@acme.com. That's the exact pattern the prompting guide uses when an agent reads a reset address to a caller. The guide's example leaves the domain as written.
Addresses
The failure: St read as "S T," Dr as "doctor" in a street name, or a floor number read as a fraction.
What the server does: US street addresses are normalized, so 54 Davis St becomes "fifty four Davis Street." In English, shorthand like 3rd Fl, c/o and w/ is written out too.
What to write: for US addresses, write them the way they appear on an envelope and let normalization do the rest. For address formats outside that list, the docs don't promise normalization, so write abbreviations out in full ("Avenue," "Suite," "Apartment") before the text reaches the TTS.
Acronyms and names
The failure: an internal acronym read as a word, or a name stressed or mangled.
What the server does: well-known abbreviations are read the expected way, so USA becomes "U S A." Uncommon ones are read as written, which may not be what you want.
What to write:
- Uncommon acronyms: put dashes between the letters.
E-T-O is Reference Evapotranspiration.If it's company jargon nobody outside says letter by letter, write the full phrase instead. - Names: there's no pronunciation dictionary, so the documented guidance is to write the reading out in words where it matters. For a name the model reads wrong, try writing it the way it's said, then test it on your voice.
- Never in all caps. Capitalizing a whole word tells AssemblyAI-TTS-1 to stress it. An LLM that writes
SMITHfrom a database field gets a shouted surname. Normalize names to title case before sending.
Two more things the server does that catch LLM output. Markdown, HTML markup, emoji and list bullets are dropped, since they're written but not said. And anything in square, curly or angle brackets is read aloud: [pause] is spoken as the word "pause." Strip those tags before you send the text.
AssemblyAI-TTS-1 normalizes phone numbers, prices, dates, emails and addresses on the server, with no SSML or pronunciation dictionary to maintain. Create a free account and run your own entity script through it.
Do you need SSML or a pronunciation dictionary?
Not with AssemblyAI-TTS-1, and the API doesn't accept either. Every reading control is plain text:
| You want | Write | Read as |
|---|---|---|
| Digits one at a time | 4-3-2-1 | four three two one |
| Digit groups with pauses | 4321-5678-9012-3456 | digit by digit, short pause after each group |
| A whole number | 4,321 | four thousand three hundred and twenty one |
| Letters one at a time | E-T-O | E T O |
| A spelled email | S-U-P-P-O-R-T@acme.com | S U P P O R T at acme dot com |
| A short pause | a comma or an ellipsis | a brief pause (stacking them doesn't lengthen it) |
| Stress on one word | You call THAT a spoon? | emphasis on "that" |
There's a practical upside. SSML is markup your LLM has to produce perfectly, and a malformed tag is a new way for a reply to fail. Plain-text conventions degrade gracefully: if the LLM forgets a dash on a phone, card or ID number, the server still normalizes it. Other bare numbers reach the model as written, so keep the formatting rule in your prompt. If you want the delivery side too (fillers, pacing, false starts), our guide to writing text that makes TTS sound human covers it.
Format entities at the LLM
The cleanest fix is upstream: get the LLM to write the spoken form. You'll get most of the way with a system prompt, and a small post-processor catches the rest.
The system prompt
Add rules like these to your agent's system prompt, whichever model you run through the LLM Gateway or elsewhere:
You are speaking on a phone call. Your replies are read aloud by a text-to-speech model.
When you say an entity:
- Write dates the way a person says them: "November 3rd", not "11/3/2026". Leave out the year unless it is needed.
- Keep the currency symbol on every amount: "$1,248.50".
- When the caller must write down or confirm a code, account number, or the last digits of a card, wrap it in <digits>...</digits> for numbers or <spell>...</spell> for codes and email usernames.
- Write names in normal title case, never in all capitals.
- Never use Markdown, bullet points, emoji, or bracketed stage directions such as [pause].
- Spell out internal abbreviations as full words.The post-processor
The markers in that prompt never reach the TTS. A post-processor turns them into the documented dash patterns and removes bracketed tags, which would otherwise be read aloud:
import re
# Markers the LLM is told to use. None of them may reach the TTS as text.
SPELL = re.compile(r"<spell>([A-Za-z0-9@._+-]+)</spell>")
DIGITS = re.compile(r"<digits>([\d\s-]+)</digits>")
BRACKET_TAGS = re.compile(r"\[[^\]]*\]") # [pause] would be read as "pause"
def spell_out(value: str) -> str:
"""Spell a code or the part of an email before the @: bx7q -> B-X-7-Q."""
local, at, domain = value.partition("@")
spelled = "-".join(ch.upper() if ch.isalpha() else ch for ch in local)
return f"{spelled}{at}{domain}"
def digit_by_digit(value: str) -> str:
"""One group -> 4-3-2-1. Several groups -> 4321-5678, read digit by digit
with a short pause between groups."""
groups = [g for g in re.split(r"[\s-]+", value.strip()) if g]
if len(groups) == 1:
return "-".join(groups[0])
return "-".join(groups)
def prepare_for_tts(reply: str) -> str:
reply = SPELL.sub(lambda m: spell_out(m.group(1)), reply)
reply = DIGITS.sub(lambda m: digit_by_digit(m.group(1)), reply)
reply = BRACKET_TAGS.sub("", reply)
return re.sub(r"\s{2,}", " ", reply).strip()
print(prepare_for_tts(
"Got it. [pause] Your code is <spell>bx7q</spell>, and I'll charge "
"the card ending in <digits>4321</digits>."
))
# Got it. Your code is B-X-7-Q, and I'll charge the card ending in 4-3-2-1.
print(prepare_for_tts("I sent it to <spell>jdoe@acme.com</spell>."))
# I sent it to J-D-O-E@acme.com.
print(prepare_for_tts("Your member ID is <digits>4321 5678 9012</digits>."))
# Your member ID is 4321-5678-9012.The tags matter because the server would otherwise turn angle brackets into round ones and read the word "spell" aloud. Run the post-processor on each sentence before you send it in a Generate frame.
If you stream LLM tokens straight to the TTS, run it per sentence rather than per token, so a marker is never split across two sends. Keep your sentence punctuation intact, since the server also uses sentence ends to decide when to start speaking.
Test it with a speech-to-text round trip
You can't fix what you don't measure, and listening to a hundred clips doesn't scale. The faster check is a round trip: synthesize every entity sentence your agent says, transcribe the audio, and compare the entities to what you sent.
Use a strong streaming model for the transcription step, such as Universal-3.6 Pro Realtime, so STT errors don't hide TTS errors. Our guide to evaluating TTS for voice agents has a full scorecard. On AssemblyAI's own pronunciation set, published in the AssemblyAI-TTS-1 launch post, AssemblyAI-TTS-1 scored 93.7% entity accuracy, against 93.6% for ElevenLabs v4, 82.5% for Cartesia Sonic 3.6 and 68.0% for ElevenLabs Flash v2.5.
The same entities cause trouble in the other direction, too. When a caller reads out a phone number in chunks with pauses, an endpointer that ends the turn at the first pause hands your LLM fragments. Universal-3.6 Pro Realtime holds the turn open on digits, codes, emails, addresses and names. Its turn detection docs report that this cuts the phone-number entity error rate from 7.0% to 2.2% in balanced mode. For the listening side of the problem, see why voice agents mishear 8 as H.
Paste the card endings, order numbers and email addresses your agent reads every day, and hear how AssemblyAI-TTS-1 says them.
Final words
Fixing entity pronunciation is mostly a formatting problem, not a model-tuning one. Let the server normalize the common forms, use dashes and commas when you need a specific reading, and teach your LLM to write the spoken form in the first place. Then measure it with a round trip, because the one entity you didn't test is the one a caller will hear wrong.
Pair Universal-3.6 Pro Realtime with AssemblyAI-TTS-1 so your agent hears and says phone numbers, codes and emails correctly. Start free, no credit card required.
Frequently Asked Questions (FAQs)
Does AssemblyAI TTS support SSML?
No. AssemblyAI-TTS-1 has no SSML or pronunciation dictionary support on either its WebSocket or HTTP API. You control readings with plain text: dashes for digit-by-digit or letter-by-letter reading, commas for whole numbers, and punctuation for pauses and stress.
How do I make text-to-speech read digits one by one?
Separate the digits with dashes, so 4-3-2-1 is read as "four three two one." For long numbers such as card or member IDs, put a dash between groups, as in 4321-5678-9012-3456. That's read digit by digit with a short pause after each group.
How do I make TTS spell out an email address?
Put dashes between the letters of the part before the @ sign, as in S-U-P-P-O-R-T@acme.com. Without dashes, AssemblyAI-TTS-1 reads a normal email address as words, such as "support at company dot com," which works for dictionary-word usernames.
Why does my voice agent mispronounce names?
Usually the name is rare, written in an unusual form, or sent in all capitals, which AssemblyAI-TTS-1 treats as stress. Normalize names to title case. Where the reading still matters, write the name out the way it's said and test it on your voice, since there's no pronunciation dictionary.
What is text normalization in TTS?
Text normalization converts written forms like $12.13, (415) 555-2876 or 10/24/2026 into the words a person would say before the speech model reads them. AssemblyAI-TTS-1 does this on the server for currencies, phone numbers, ID numbers, emails, web addresses, measurements, dated numbers and US street addresses, in every supported language.
How do I test whether a voice agent pronounces entities correctly?
Run a speech-to-text round trip: synthesize each entity sentence, transcribe the audio with an accurate streaming model, and compare the entities to the input text. Score each entity type separately, since phone numbers, emails and names fail in different ways.