Catalog & providers
Catalog & providers API
The catalog lists the LLM, speech-to-text, text-to-speech, and telephony providers (and their models, pricing, and capabilities) available to your agents. Use it to discover the valid llmProvider/llmModel/sttProvider/ttsProvider/ttsVoice identifiers you pass when creating an agent, and the pricing that drives cost estimates.
Get the catalog
GET /api/catalog
Requires authentication (API key or JWT). The response groups entries by kind and merges in the global platform fee used in cost estimates.
curl https://api.telenow.ai/api/catalog -H "x-api-key: vai_live_…"
{
"success": true,
"data": {
"llm": [
{
"id": "openai",
"name": "OpenAI",
"blurb": "…",
"keyEnvVar": "OPENAI_API_KEY",
"status": "ga",
"latency": { "ms": 420, "tier": "fast" },
"models": [
{
"id": "gpt-4o-mini",
"name": "GPT-4o mini",
"contextWindow": 128000,
"supportsFunctions": true,
"supportsCaching": true,
"pricing": {
"inputPerM": 0.15,
"outputPerM": 0.60,
"cachedInputPerM": 0.075
},
"latency": { "ms": 420, "tier": "fast" }
}
]
}
],
"stt": [
{
"id": "deepgram",
"name": "Deepgram",
"realtime": true,
"languages": ["en", "hi"],
"perMinuteUsd": 0.0043,
"models": [
{ "key": "nova-2", "name": "Nova 2", "configKey": "model", "perMinuteUsd": 0.0043 }
]
}
],
"tts": [
{
"id": "elevenlabs",
"name": "ElevenLabs",
"streaming": true,
"voices": [ /* … */ ],
"perMillionCharsUsd": 300,
"models": [
{ "key": "eleven_flash_v2_5", "name": "Flash v2.5", "configKey": "model_id", "perMillionCharsUsd": 150 }
]
}
],
"telephony": [
{
"id": "plivo",
"name": "Plivo",
"capabilities": { /* … */ },
"perMinuteUsd": 0.014
}
],
"platformFee": { "percent": 0, "perMinUsd": 0 }
}
}
Every field, in plain English
On every provider, of every kind:
| Field | What it is |
|---|---|
id | The string you store in llmProvider / sttProvider / ttsProvider / telephonyProvider / s2sConfig.provider. Unique within a kind, not across them — openai appears under llm, tts and s2s as three unrelated rows. |
name | The display name, e.g. "Sarvam (Indian languages)". |
blurb | A sentence or two on what it is and when to pick it. null if nobody wrote one. |
keyEnvVar | The name of the server environment variable holding the platform's key for this provider. Documentation only — no key value is ever sent. null for providers that need no platform key (the custom* ones, telenow_smart, and carriers that take per-org credentials). |
status | "ga" or "preview". Never null. |
latency | { "ms": 250, "tier": "low" } — a representative time to first audio or first token, and a bucket (realtime, low, medium, high). ms: 0 means "not measured", not "instant". |
recommended | The platform's Recommended mark. It also drives order: recommended rows sort first. |
enabled | Always true here — disabled rows are filtered out of this response entirely. The field exists for the admin console, which asks for disabled rows too. |
Present only when the provider has them — these keys are omitted, not set to null:
| Field | What it is |
|---|---|
multilingual | This provider covers many languages with one configuration. |
configFields | The form schema for this provider's own settings and BYOK key. Each entry has at least key and kind (text, number, select, toggle, json); optionally label, defaultValue, options, min/max/step, placeholder, help, sensitive (mask it), advanced. Treat everything but key and kind as optional. |
docs | A help block: { "intro": "...", "sections": [{ "label", "body", "language" }] }. Rare. |
Then, by kind:
| Kind | Adds |
|---|---|
llm | models[]. Each model: id, name, contextWindow, supportsFunctions, supportsCaching, recommended, enabled, and pricing — { inputPerM, outputPerM, cachedInputPerM } in USD per million tokens. pricing is null when no rate is set; cachedInputPerM is null when cached tokens bill at the normal input rate. |
stt | realtime (can it stream), languages (array of codes), perMinuteUsd (the provider default), models[]. Each model: key, name, configKey, perMinuteUsd (null inherits the provider rate), recommended, enabled. |
tts | streaming, voices[] (see below), perMillionCharsUsd (provider default), capabilities, models[]. Each model: key, name, configKey, perMillionCharsUsd (null inherits), recommended, enabled. |
telephony | capabilities, perMinuteUsd — the carrier's per-minute rate. |
s2s | languages, models[]. The whole s2s key is absent unless this deployment has the realtime engine on — check features.s2s, or use GET /api/catalog/s2s. |
capabilitiesis where voice cloning is announced. On a TTS provider it is{ "cloning": true }for the seven engines that support it (telenow, elevenlabs, cartesia, lmnt, smallest, soniox, xai) and{}otherwise. On a telephony provider it describes what the carrier can do. It is an open-ended object — read the key you need and ignore the rest.
A TTS
voices[]entry looks like{ "id": "anushka", "name": "Anushka", "gender": "female", "language": "hi-IN" }.idis what you put inttsVoice.modelappears when the provider's voices are split across models (Sarvam, Rime, Smallest, Telenow) — a voice is only usable with the model it names.tags,accent,previewUrl,styleanddescriptionappear on some providers. Onlyidandnameare guaranteed.
Watch the identifier key. LLM models call it
id; STT and TTS models call itkey; the realtime roster at/api/catalog/s2scalls itmodelKey. Same concept, three spellings — this is a wart, not a signal.
Per-model pricing matters. LLM, STT, and TTS pricing is resolved at the model level — a faster/cheaper model under the same provider can cost far less. Always read the model's own
pricing/perMinuteUsd/perMillionCharsUsdrather than assuming the provider default.
Some models are hidden. Reasoning-capable LLM models are stripped from this response unless the deployment has reasoning switched on. If a model you expect is missing, that is the likely reason.
Deployment features
Alongside the provider kinds, the bundle carries a features map describing what this deployment has switched on:
"features": { "providerFallback": false, "s2s": true }
features.s2s is the one to check before offering a realtime engine: when it is false the s2s key is absent from the bundle entirely, and features.s2s is what tells you that absence means "disabled here" rather than "older backend".
Prompt context
The bundle also carries a promptContext block: measured token counts for the instructions and tool schemas Telenow adds to every agent prompt, on top of whatever system prompt you write.
It is there so a cost estimate can include them. If you are building your own estimator, add these to your own prompt's tokens — a stock agent's real per-request input is meaningfully larger than its visible system prompt. Render only the keys you find; the set can grow.
Realtime (speech-to-speech) roster
GET /api/catalog/s2s
The realtime engines, their models, and each model's voices with the metadata you need to build a picker. This is the endpoint to use when configuring s2sConfig on an agent — the provider/model/voice triple it returns is exactly what agent create/update validates against.
Requires authentication (API key or JWT).
curl https://api.telenow.ai/api/catalog/s2s -H "x-api-key: vai_live_…"
{
"success": true,
"data": {
"enabled": true,
"providers": [
{
"id": "openai",
"name": "OpenAI Realtime",
"blurb": "One realtime model handles listening, thinking and speaking…",
"status": "ga",
"recommended": false,
"languages": ["en", "hi", "es", "…"],
"models": [
{
"id": "gpt-realtime",
"name": "GPT Realtime",
"contextWindow": 32000,
"voices": [
{
"id": "marin",
"name": "Marin",
"gender": "female",
"accent": "American",
"tags": ["natural", "warm"],
"recommended": true
}
]
}
]
}
]
}
}
| Field | Meaning |
|---|---|
enabled | false when the realtime engine is off on this deployment. providers is then [] — the endpoint returns 200, never 404, so "disabled" is always distinguishable from "wrong URL". |
providers[].languages | Engine-level. Realtime models auto-detect and code-switch, so every voice of an engine speaks all of them — there is no per-voice language. |
models[].id | The value for s2sConfig.model. |
voices[].id | The value for s2sConfig.voice. |
voices[].gender / accent | accent is the voice's primary English accent; these are multilingual timbres that adapt to whatever language they speak. |
voices[].tags | Vendor-documented character/style words. |
voices[].recommended | Float these to the top of a picker. |
Unlike
GET /api/catalog, this endpoint carries no pricing. Realtime models are priced per audio and text token; see Usage & billing for what a call costs.
To hear a voice before you save it, see Voice preview.
Using catalog values
When creating or updating an agent, set the provider/model fields to identifiers from the catalog:
{
"llmProvider": "openai",
"llmModel": "gpt-4o-mini",
"sttProvider": "deepgram",
"ttsProvider": "elevenlabs",
"ttsVoice": "rachel"
}
The dashboard agent builder reads the same catalog, so any combination it offers is valid over the API.
OpenRouter price lookup
OpenRouter models are entered as a free-text model id (any model OpenRouter exposes), so they aren't always in the synced catalog. This endpoint returns the sell price for a given OpenRouter model id — the rate your org will actually be billed (synced rate-card price × (1 + margin%)). It powers the builder's $/min estimate and price datalist as you type.
GET /api/catalog/openrouter-price?model=<id>
Requires authentication — an org API key or a JWT, the same as every other catalog route. On a sync miss (a model newer than the last daily sync), the server live-fetches OpenRouter's public price list once and upserts what it finds, then retries — so a typo at worst triggers one bounded fetch.
curl "https://api.telenow.ai/api/catalog/openrouter-price?model=anthropic/claude-3.5-sonnet" \
-H "x-api-key: vai_live_…"
{
"success": true,
"data": {
"found": true,
"modelKey": "anthropic/claude-3.5-sonnet",
"inputPerM": 3.45,
"outputPerM": 17.25,
"marginPercent": 15
}
}
| Field | Meaning |
|---|---|
found | false when the model id is empty or unknown to OpenRouter (the only key returned in that case). |
modelKey | The model id echoed back. |
inputPerM | Sell price per 1M input tokens (USD), already including the margin. |
outputPerM | Sell price per 1M output tokens (USD), already including the margin. |
marginPercent | The margin percentage applied over OpenRouter's cost. |
Actual OpenRouter billing is cost-plus on OpenRouter's reported per-request cost; this endpoint is the matching estimate the builder shows.
Every provider this platform supports
GET /api/catalog returns what your deployment currently offers. The tables below are the full set the platform implements, so you can tell "not offered here" apart from "does not exist".
Text-to-speech
| Provider id | Name | Platform key | Streams | Cloning | Where its voices come from |
|---|---|---|---|---|---|
telenow | Telenow TTS | TELENOW_TTS_API_KEY | yes | yes | Live, plus 52 curated. The only provider with a live model list. Voices are tagged per model. |
elevenlabs | ElevenLabs | ELEVENLABS_API_KEY | yes | yes | Live (your account's premade + cloned voices), plus 20 curated, plus the shared library below. |
openai | OpenAI TTS | OPENAI_API_KEY | no | no | Catalog only — 6 voices. No live list. |
cartesia | Cartesia | CARTESIA_API_KEY | yes | yes | Live (all pages). Only 2 curated — use the live route. |
polly | Amazon Polly | AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY | yes | no | Live. Its models set configKey: "engine", not "model". |
lmnt | LMNT (Aurora) | LMNT_API_KEY | yes | yes | Live. Multilingual, so voices carry no per-voice language. |
rime | Rime | RIME_API_KEY | yes | no | Live (~863 voices vs 44 curated). Speaker sets are disjoint per model — always match voice.model to your model. |
sarvam | Sarvam (Indian languages) | SARVAM_API_KEY | yes | no | Catalog only — 45 voices, tagged bulbul:v2 / bulbul:v3. The live route answers source: "fallback" with []. |
smallest | Smallest.ai (Lightning) | SMALLEST_API_KEY | yes | yes | Live, per model. 229 curated — the largest curated set. |
hume | Hume (Octave) | HUME_API_KEY | no | no | Live (all pages). |
xai | xAI (Grok) | XAI_API_KEY | yes | yes | Live — stock voices merged with your account's custom ones. |
soniox | Soniox | SONIOX_API_KEY | yes | yes | Live. Shares its key with Soniox STT. |
google | Google Gemini TTS | GEMINI_API_KEY | yes | no | Catalog only — 30 voices, each of which speaks every supported language. Accent is not a property of the voice: you write it into the provider's accent / scene / directorNotes fields and the voice performs it. Shares its key with the Gemini LLM and realtime. |
unrealspeech | UnrealSpeech | UNREAL_SPEECH_API_KEY | yes | no | Catalog only — 15 voices. |
customtts | Custom TTS | none | yes | no | Neither — you describe your own endpoint in ttsConfig. See Custom models. |
Two things that surprise people: Deepgram has TTS model rows but no TTS provider row, so Deepgram TTS never appears under tts. And opentts appears in the legacy /api/providers/tts list but is not implemented — ignore it.
google is different from every other entry in this table, because Gemini TTS is directed, not parameterised. There is no speed or pitch; you brief it the way you would brief an actor, and the same 30 voices cover every language and accent:
"ttsConfig": {
"provider": "google",
"voice": "Kore",
"model": "gemini-3.1-flash-tts-preview",
"audioProfile": "A 32-year-old support lead at an Indian fintech. Patient, never salesy.",
"scene": "An inbound call about a failed UPI payment. The caller is mildly frustrated.",
"directorNotes": "Unhurried. Read the reference number one digit group at a time.",
"accent": "Indian English, neutral urban"
}
Three consequences worth knowing before you use it:
- The brief is part of the voice. Change
sceneand you change the performance, so it is hashed into the TTS cache key alongside provider and voice — cached greetings and fillers are re-synthesised rather than re-used across a scene change. - A turn is synthesised whole. Every other engine here streams clause by clause; this one buffers the complete reply and makes one request, because a second request is a second take and the delivery drifts between them. The trade is that the first audio frame waits for the last token of the reply instead of the first clause.
- Inline tags are performed, not read out. Set
"expressive": true(the builder's Expression tags toggle, on by default for this provider) and the model may direct its own delivery clause by clause. Your tag vocabulary is translated to Google's words where they differ —[whispering]goes out as[whispers],[laugh]as[laughs],[chuckle]as[giggles],[sigh]as[sighs]— and anything descriptive that Google doesn't list ([reassuring],[hesitantly],[apologetic]) is forwarded as-is, because Google documents an open tag parser with no fixed list. Three sounds have no Gemini equivalent and are dropped rather than approximated:[sharp_inhale],[stammer],[trail_off].
Speech-to-text
deepgram, sarvam, soniox, elevenlabs, xai, google, telenow_smart, azure, groq, aws_transcribe, customstt. Languages are per provider in stt[].languages.
google is Gemini 3.5 Transcribe (live) over the Gemini Live API: streaming partials, and automatic language detection that code-switches inside a single sentence with no configuration — leave language on auto for Indian calls rather than pinning a locale.
Tamil is not supported by this recogniser. It covers
en-IN,hi-IN,bn-IN,gu-IN,kn-IN,ml-IN,mr-IN,or-IN,pa-IN,te-INandas-IN, but notta-INand not Urdu. Route Tamil traffic tosarvamordeepgram.
It reports no confidence scores and no per-word timings, so the low-confidence short-final guard that protects other multilingual recognisers never fires here. Google closes a Live session at ten minutes; the provider rotates its socket before that, which is invisible on the call.
Realtime (speech-to-speech)
openai and gemini only. s2sConfig.provider also accepts google as an alias for gemini.
Getting a provider's voices
There are two voice lists and they answer different questions. Build your picker from the catalog; refresh it from the live route.
1. The catalog list — curated, authenticated, no vendor call
Every TTS provider in GET /api/catalog carries a voices array. It is instant, it needs no vendor key, and it is the only source for providers with no live API (sarvam, openai, unrealspeech, customtts).
curl -s https://api.telenow.ai/api/catalog -H "X-API-Key: $TELENOW_API_KEY" \
| jq -r '.data.tts[] | "\(.id)\t\(.voices|length)\t\(.name)"'
One provider, filtered to the model you will actually use:
curl -s https://api.telenow.ai/api/catalog -H "X-API-Key: $TELENOW_API_KEY" \
| jq '.data.tts[] | select(.id=="sarvam") | .voices[] | select(.model=="bulbul:v3")'
2. The live list — anonymous, asks the vendor right now
GET /api/providers/tts/{provider}/voices
No authentication. Optional query params: apiKey to read your own vendor account rather than the platform's (useful before the agent is saved), and model for providers whose voices differ per model.
# the platform account's ElevenLabs voices
curl -s https://api.telenow.ai/api/providers/tts/elevenlabs/voices | jq '.source, (.data|length)'
# your own account, before saving anything
curl -s "https://api.telenow.ai/api/providers/tts/elevenlabs/voices?apiKey=sk_YOUR_KEY" | jq '.data[0]'
# a provider whose speakers differ per model
curl -s "https://api.telenow.ai/api/providers/tts/telenow/voices?model=omnivoice" | jq '.data | length'
⚠️ This endpoint never returns an error. A wrong key, an outage, or a provider with no voices API all come back 200 with "source": "fallback" — for ElevenLabs that is six hard-coded ids, for everyone else an empty array. Always branch on source:
{ "success": true, "source": "live", "data": [ … ] } ← the vendor answered
{ "success": true, "source": "fallback", "data": [] } ← it did not; fall back to the catalog list
Do not read an empty list as "this key has no voices". Treat source: "fallback" as "unknown" and show the catalog list instead.
Note the casing difference: catalog voices are camelCase (previewUrl), live voices are snake_case (preview_url).
Models for a provider
# works for every provider — and tells you WHICH config key to write
curl -s https://api.telenow.ai/api/catalog -H "X-API-Key: $TELENOW_API_KEY" \
| jq '.data.tts[] | select(.id=="polly") | .models'
# → [{"key":"neural","name":"Neural","configKey":"engine", …}]
configKey is the field name inside ttsConfig that selects the model — usually "model", but Polly uses "engine". Write the key it names, not the one you assume.
GET /api/providers/tts/{provider}/models also exists but is only implemented for telenow.
The ElevenLabs shared library
Thousands of community voices beyond your account:
curl -s "https://api.telenow.ai/api/providers/tts/elevenlabs/library?search=indian&gender=female&pageSize=30" \
| jq '{total: .totalCount, hasMore, first: .data[0]}'
Browsing is anonymous. Adding one is authenticated, consumes a voice slot in the target ElevenLabs account, and returns the id you must actually use — which may differ from the one you browsed:
curl -s -X POST https://api.telenow.ai/api/providers/tts/elevenlabs/library/add \
-H "X-API-Key: $TELENOW_API_KEY" -H "Content-Type: application/json" \
-d '{"publicOwnerId":"…","voiceId":"…","name":"Priya (support)","apiKey":"sk_YOUR_ELEVENLABS_KEY"}'
# → {"success": true, "voiceId": "<use THIS id>"}
Send your own
apiKey. Without it the voice is added to the shared platform account, where it counts against a slot limit everyone shares.
Realtime voices
Realtime voices are per model, and there is no live vendor route — the catalog is the only source:
curl -s https://api.telenow.ai/api/catalog/s2s -H "X-API-Key: $TELENOW_API_KEY" \
| jq '.data.providers[] | {id, models: [.models[] | {id, voices: [.voices[].id]}]}'
A provider is missing from the response
Work down this list:
- A platform admin switched it off. This is by far the most common cause. Disabled rows are removed from
GET /api/catalogand/api/public/pricingcompletely — not returned withenabled: false. Admins: the Providers page has the toggle; the TTS voices page now shows the same state, so a provider listed there with a "Not offered" badge is exactly this case. - The whole
s2skey is gone — the deployment has the realtime engine off. Checkfeatures.s2s. - A specific LLM model is gone — it is reasoning-capable and reasoning is off for this deployment.
- It never existed here. Compare against the roster above;
deepgramTTS andopenttsare the usual suspects.
Marketing pages, docs and SDK samples can list providers a given deployment does not serve. GET /api/catalog is the only authority for what you can actually select.
Other endpoints
| Endpoint | Auth | Returns |
|---|---|---|
GET /api/catalog | API key or JWT | The whole bundle. |
GET /api/catalog/s2s | API key or JWT | The realtime roster: providers → models → decorated voices, and an enabled flag. Voices here always carry gender, accent and tags; where the vendor publishes nothing, sensible defaults are filled in — treat them as presentation hints, not facts. |
GET /api/catalog/openrouter-models | API key or JWT | OpenRouter's live model list, each tagged textToText. Voice agents only support text-in/text-out models. Returns [] on a fetch failure rather than an error. |
GET /api/catalog/openrouter-price | API key or JWT | Paste-time price lookup for a free-text OpenRouter model id. |
GET /api/agent-defaults | API key or JWT | The recommended starting stack for a new agent — the answer to "which providers should I put in my create body?". |
GET /api/providers/tts/{p}/voices | none | Live voice list. See above. |
GET /api/providers/tts/{p}/models | none | Live model list. telenow only. |
GET /api/providers/tts/elevenlabs/library | none | The ElevenLabs shared voice library. |
POST /api/providers/tts/elevenlabs/library/add | API key or JWT | Adds a library voice to an account. |
GET /api/providers/llm · /stt · /tts · /telephony | none | Legacy hard-coded summaries. They have drifted from reality — they list providers that are not implemented and omit ones that are. Use GET /api/catalog. |
There is no GET /api/providers root.
Everything under /api is rate-limited: roughly 2,000 requests per 15 minutes for anonymous callers (keyed on IP), 3,000 for authenticated ones (keyed on user).
Model availability and pricing are managed centrally and change over time. Fetch the catalog at runtime rather than hard-coding lists.
Related
- Usage & billing — how component charges and the platform fee are computed from catalog prices.
- Custom models — bringing your own model endpoints and keys.
- Agents API — where these provider/model identifiers are used.
Provider endpoints
/api/providers/* sits alongside /api/catalog. The read endpoints are public — no auth; the two that open a vendor session are authenticated.
| Method | Path | Auth |
|---|---|---|
GET | /api/providers/llm | public |
GET | /api/providers/stt | public |
GET | /api/providers/tts | public |
GET | /api/providers/telephony | public |
GET | /api/providers/tts/{provider}/voices | public |
GET | /api/providers/tts/{provider}/models | public |
GET | /api/providers/tts/{provider}/library | needs a key for that provider |
POST | /api/providers/tts/{provider}/library/add | authenticated |
POST | /api/providers/custom-endpoint/test | authenticated |
/library browses the vendor's own voice catalogue rather than ours, so it opens a session against the provider — it returns 400 without a usable key for that provider (ElevenLabs, for example). /library/add copies a voice from that catalogue into your workspace. custom-endpoint/test probes a self-hosted model before you save it onto an agent; see Custom LLM & API.