Ask five text-to-speech providers to describe the same voice and you get five different objects. One calls the language field Locale, another languageCodes, a third just language. One ships a typed list of speaking styles, the rest ship nothing comparable. Sample rate is a property of the voice for two of them and a parameter of the synthesis request for the others.
The names are the easy part. The harder problem is that fields which look equivalent, like locale, gender, style, age, and sample rate, often carry different guarantees and mean different things operationally. This page is a field-contract audit of how Telnyx, ElevenLabs, Azure, AWS Polly, and Google Cloud describe voice metadata today, and a conservative schema for reconciling them without inventing data.
TL;DR
- The providers represent language with locale-like tags, but the contract is not uniform: Google explicitly returns BCP-47 in
languageCodes, Azure and Polly commonly return language-region codes, ElevenLabs combines free-form labels with optional verified-language metadata, and Telnyx exposes a single normalizedlanguagestring - Gender is the most common voice attribute, but it is neither semantically uniform nor guaranteed per voice: Polly uses a two-value field, Google returns an SSML enum whose
NEUTRALvalue is currently unsupported, and ElevenLabs derives it from an optional label - Azure is the strongest source of typed expressive metadata through
StyleListandRolePlayList; ElevenLabs offers label-based discovery metadata; Polly and Google's basic voice objects expose no comparable per-voice style array - Native sample rate and requested output rate are separate concepts, and only Azure and Google report a native rate on the voice
- Normalize conservatively: keep every raw provider value, canonicalize a language tag only when it validates, never derive an accent from a region, and separate invocable styles from descriptive labels
Why there is no standard
TTS metadata grew one vendor at a time. AWS Polly launched around a small fixed roster of voices, so its schema is a short enum: pick a Name, get a Gender and a LanguageCode. Azure built its catalog for a speech platform with expressive SSML, so its voice records carry style and role data that only make sense alongside that markup. ElevenLabs came up through voice cloning and a community voice library, so its schema is built around user-supplied labels and per-language verification. Google keeps its voice object minimal and pushes voice character into the model name.
Telnyx sits in front of most of these, exposing one list endpoint across its native models and a changing set of third-party providers. TTS Library, the site you are reading this on, then layers its own enrichment on top of that. Both are examples of the raw-versus-normalized split this schema formalizes, and neither is a standard the other providers follow.
Locale codes
Every provider represents language with a locale-like tag, but the contract behind that tag differs.
| Provider | Field | What you actually get |
|---|---|---|
languageCodes |
Documented BCP-47 tags, including macroregions like es-419 and cmn-tw |
|
| AWS Polly | LanguageCode, AdditionalLanguageCodes |
ISO-based language-region codes such as en-US and en-GB-WLS; bilingual voices list extra codes |
| Azure | Locale, SecondaryLocaleList |
Language-region strings such as zh-CN; multilingual voices list secondary locales |
| ElevenLabs | labels.language plus verified_languages[] |
A free-form label (often a bare subtag) plus an optional, nullable array with per-language locale and accent |
| Telnyx | language |
A single normalized string; the docs do not guarantee a full locale |
A safe rule is to keep the provider's value as locale_raw, canonicalize it to BCP-47 casing only when it validates as a language tag, and treat language, region, script, and accent as independently nullable. A tag can legitimately be language-only. A region is not a reliable proxy for accent, and es-419 is a macroregion rather than a country. Script can matter for languages like Chinese (zh-Hans versus zh-Hant), though Google specifically advises omitting the script subtag in its voice-selection field because it infers script from the input text.
Do not derive an accent just because a region subtag is present.
Gender taxonomy
Gender is the most consistently available attribute, and it is still not something you can treat as one field.
| Provider | Field | Values |
|---|---|---|
| AWS Polly | Gender |
Female, Male |
| Azure | Gender |
Female, Male, occasionally Neutral |
ssmlGender |
SSML_VOICE_GENDER_UNSPECIFIED, MALE, FEMALE, NEUTRAL (Google documents NEUTRAL as not yet supported) |
|
| ElevenLabs | labels.gender |
Free text from an optional label, usually male, female, or neutral |
| Telnyx | gender |
A string in the list-voices response; no documented multi-value taxonomy |
Google's value is an enum tied to SSML voice selection, which is why it is uppercase and carries an explicit unspecified state. ElevenLabs stores gender as a user-applied label, so casing and vocabulary are not guaranteed and the label can be missing.
A source-preserving normalization keeps both forms:
gender_normalized: female | male | neutral | unknown
gender_raw: string | null
TTS Library's own normalization layer (lib/normalizeGender.js) collapses upstream casing variants and also emits Non-binary where an upstream source provides it. Only emit non-binary when a real provider value backs it, not as a default.
Age
Most providers have no age field at all.
| Provider | Field | Notes |
|---|---|---|
| ElevenLabs | labels.age |
Optional, free-form; values like young, middle_aged, old appear in practice |
| MiniMax (via Telnyx) | age |
Coarse labels such as adult, surfaced through the proxied catalog |
| Azure | RolePlayList |
Expressive role-play personas, not demographic age (see below) |
| AWS Polly, Google | none | Age is implied by the voice, never labeled |
Azure's RolePlayList is the field most often mistaken for an age model. It is a list of role-play personas the voice supports through Azure's expressive SSML, mixing age-coded, gendered, and narrative labels like YoungAdultFemale, OlderAdultMale, Boy, Girl, and Narrator. A voice that lists Boy is not necessarily a child voice in the catalog; it can perform that persona on request. Treat these as supported synthesis personas, not as the voice's inherent age.
If you need an age dimension across a catalog, plan to infer it for most voices rather than read it.
Style and emotion
This is where the catalogs diverge hardest, and where it is easy to overstate.
Azure ships a typed StyleList per voice, with values such as cheerful, angry, sad, excited, friendly, whispering, newscast, customerservice, and narration-relaxed. Each style is something you then invoke through the mstts:express-as SSML tag. This is the clearest example of a per-voice, invocable style field in any of the five catalogs.
ElevenLabs has no equivalent style enum, but it does carry style-adjacent discovery metadata: its list and shared-voice endpoints filter on use_cases (narrative_story, conversational, characters), descriptives (free-text descriptors like calm or raspy), and the labels map. Those help you find a voice, but they are semi-structured discovery hints, not a guaranteed synthesis-control vocabulary.
AWS Polly and Google's basic voice-list objects expose no comparable per-voice style array. Expressiveness on those platforms comes from engine or model choice (Polly's generative engine, Google's newer promptable and controllable voices) and from SSML, rather than from a tag on the voice record. Telnyx's list-voices response is intentionally compact and carries no style field, though individual Telnyx models document expressive controls (Telnyx Ultra exposes emotion, speed, and volume).
For a unified object, split what the draft schema would have called styles[] into three fields so an ElevenLabs descriptor is never mistaken for an invocable Azure style:
styles_supported: string[] # explicit, invocable style names (Azure StyleList)
descriptors: string[] # search and discovery labels, not contractual controls
expressive_controls: string[] # e.g. ssml, prompt, speed, pitch, emotion
Sample rate
Sample rate is metadata on some platforms and a request parameter on others, and the two are worth modeling separately.
| Provider | Native rate on the voice | Requested output rate |
|---|---|---|
| Azure | SampleRateHertz, 24000 or 48000 (high-fidelity models report 48000) |
X-Microsoft-OutputFormat header; resampled if it differs from the model |
naturalSampleRateHertz (commonly 24000) |
AudioConfig.sampleRateHertz; Google converts and warns on quality if it differs |
|
| AWS Polly | none | SampleRate on SynthesizeSpeech: mp3 and ogg_vorbis accept 8000, 16000, 22050, 24000, 44100, 48000; pcm accepts 8000 or 16000. Standard defaults to 22050, neural, long-form, and generative to 24000 |
| ElevenLabs | none | output_format on the synthesis request encodes both codec and rate, for example mp3_44100_128 or pcm_16000 |
| Telnyx | none in the list response | Chosen per synthesis call, codec dependent |
Store native_sample_rate_hz only when the catalog explicitly provides one. Model supported_output_formats[] and requested_sample_rate_hz separately, because output selection is a request-level, codec-dependent choice. Azure and Google document conversion when the requested rate differs from the native rate; do not assume the same behavior for every provider.
Provider-specific extensions
Fields that only one provider has. Keep these in an extensions object rather than forcing them into the core schema:
| Provider | Extension fields |
|---|---|
| AWS Polly | SupportedEngines (standard, neural, long-form, generative) |
| Azure | VoiceType, Status (GA or Preview), WordsPerMinute, SecondaryLocaleList |
| ElevenLabs | category (premade, cloned, professional, generated), fine_tuning, verified_languages |
| Telnyx and MiniMax | model name and quality tier, hosted flag |
A proposed integration schema
This is a schema for your normalization layer, not a contract any provider publishes. It separates four kinds of information: what the provider claimed (*_raw), a canonical identifier derived only when it validates, an inference you made and can explain (*_source), and a synthesis capability.
| Field group | Treatment | Why |
|---|---|---|
| Locale | locale_raw plus nullable locale_bcp47, language, region, script |
Do not convert a free-form label or partial code into a canonical locale silently |
| Accent | accent_raw, accent_source, optional accent_normalized |
A region is not a reliable proxy for accent |
| Gender | gender_raw plus gender_normalized |
Preserves optional and free-text upstream values |
| Age | age_label_raw, age_normalized, role_play[] |
Azure role-play is a capability, not an age field |
| Style | styles_supported[], descriptors[], expressive_controls[] |
Separates invocable controls from discovery labels |
| Sample rate | native_sample_rate_hz, requested_sample_rate_hz, supported_output_formats[] |
Catalog metadata is not per-request configuration |
| Engine | engine, model_id, supported_engines[] |
"Tier" is not one shared cross-vendor concept |
| Extensions | extensions{} |
The right home for lossless provider-specific data |
A defensible core object:
{
"voice_id": "aws.polly.Joanna",
"provider": "aws_polly",
"provider_voice_id": "Joanna",
"name": "Joanna",
"locale_raw": "en-US",
"locale_bcp47": "en-US",
"language": "en",
"region": "US",
"script": null,
"accent_raw": null,
"accent_normalized": null,
"accent_source": null,
"gender_raw": "Female",
"gender_normalized": "female",
"age_label_raw": null,
"age_normalized": null,
"role_play": [],
"styles_supported": [],
"descriptors": [],
"expressive_controls": ["ssml"],
"native_sample_rate_hz": null,
"supported_output_formats": [],
"supported_engines": ["standard", "neural"],
"extensions": {}
}
Telnyx and TTS Library as normalization layers
Telnyx's catalog endpoint, GET https://api.telnyx.com/v2/text-to-speech/voices, returns a compact object with provider, name, voice_id, language, and gender, across its native models and a changing set of third-party providers. Telnyx's public materials advertise more than 3,900 voices across more than 80 languages; treat the exact provider set and count as time-sensitive.
TTS Library layers additional enrichment on that catalog: an accent classification, resolved language_name and country_name from a locale map, and a normalized gender. You can see the enriched shape at /developers and query it at /api/voices. It is one concrete instance of the raw-plus-normalized pattern above, not a reference the other providers implement.
The takeaway holds across all five: fields that look equivalent often differ in provenance and operational meaning. Normalize conservatively, keep the raw values, and record how every inference was made.
For background on the technology these voices come from, see what is TTS and what is neural TTS.