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:

FieldWhat it is
idThe 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.
nameThe display name, e.g. "Sarvam (Indian languages)".
blurbA sentence or two on what it is and when to pick it. null if nobody wrote one.
keyEnvVarThe 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".
recommendedThe platform's Recommended mark. It also drives order: recommended rows sort first.
enabledAlways 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:

FieldWhat it is
multilingualThis provider covers many languages with one configuration.
configFieldsThe 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.
docsA help block: { "intro": "...", "sections": [{ "label", "body", "language" }] }. Rare.

Then, by kind:

KindAdds
llmmodels[]. 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.
sttrealtime (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.
ttsstreaming, voices[] (see below), perMillionCharsUsd (provider default), capabilities, models[]. Each model: key, name, configKey, perMillionCharsUsd (null inherits), recommended, enabled.
telephonycapabilities, perMinuteUsd — the carrier's per-minute rate.
s2slanguages, models[]. The whole s2s key is absent unless this deployment has the realtime engine on — check features.s2s, or use GET /api/catalog/s2s.

capabilities is 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" }. id is what you put in ttsVoice. model appears 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, style and description appear on some providers. Only id and name are guaranteed.

Watch the identifier key. LLM models call it id; STT and TTS models call it key; the realtime roster at /api/catalog/s2s calls it modelKey. 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/perMillionCharsUsd rather 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
              }
            ]
          }
        ]
      }
    ]
  }
}
FieldMeaning
enabledfalse 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[].languagesEngine-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[].idThe value for s2sConfig.model.
voices[].idThe value for s2sConfig.voice.
voices[].gender / accentaccent is the voice's primary English accent; these are multilingual timbres that adapt to whatever language they speak.
voices[].tagsVendor-documented character/style words.
voices[].recommendedFloat 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
  }
}
FieldMeaning
foundfalse when the model id is empty or unknown to OpenRouter (the only key returned in that case).
modelKeyThe model id echoed back.
inputPerMSell price per 1M input tokens (USD), already including the margin.
outputPerMSell price per 1M output tokens (USD), already including the margin.
marginPercentThe 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 idNamePlatform keyStreamsCloningWhere its voices come from
telenowTelenow TTSTELENOW_TTS_API_KEYyesyesLive, plus 52 curated. The only provider with a live model list. Voices are tagged per model.
elevenlabsElevenLabsELEVENLABS_API_KEYyesyesLive (your account's premade + cloned voices), plus 20 curated, plus the shared library below.
openaiOpenAI TTSOPENAI_API_KEYnonoCatalog only — 6 voices. No live list.
cartesiaCartesiaCARTESIA_API_KEYyesyesLive (all pages). Only 2 curated — use the live route.
pollyAmazon PollyAWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEYyesnoLive. Its models set configKey: "engine", not "model".
lmntLMNT (Aurora)LMNT_API_KEYyesyesLive. Multilingual, so voices carry no per-voice language.
rimeRimeRIME_API_KEYyesnoLive (~863 voices vs 44 curated). Speaker sets are disjoint per model — always match voice.model to your model.
sarvamSarvam (Indian languages)SARVAM_API_KEYyesnoCatalog only — 45 voices, tagged bulbul:v2 / bulbul:v3. The live route answers source: "fallback" with [].
smallestSmallest.ai (Lightning)SMALLEST_API_KEYyesyesLive, per model. 229 curated — the largest curated set.
humeHume (Octave)HUME_API_KEYnonoLive (all pages).
xaixAI (Grok)XAI_API_KEYyesyesLive — stock voices merged with your account's custom ones.
sonioxSonioxSONIOX_API_KEYyesyesLive. Shares its key with Soniox STT.
googleGoogle Gemini TTSGEMINI_API_KEYyesnoCatalog 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.
unrealspeechUnrealSpeechUNREAL_SPEECH_API_KEYyesnoCatalog only — 15 voices.
customttsCustom TTSnoneyesnoNeither — 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 scene and 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-IN and as-IN, but not ta-IN and not Urdu. Route Tamil traffic to sarvam or deepgram.

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:

  1. A platform admin switched it off. This is by far the most common cause. Disabled rows are removed from GET /api/catalog and /api/public/pricing completely — not returned with enabled: 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.
  2. The whole s2s key is gone — the deployment has the realtime engine off. Check features.s2s.
  3. A specific LLM model is gone — it is reasoning-capable and reasoning is off for this deployment.
  4. It never existed here. Compare against the roster above; deepgram TTS and opentts are 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

EndpointAuthReturns
GET /api/catalogAPI key or JWTThe whole bundle.
GET /api/catalog/s2sAPI key or JWTThe 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-modelsAPI key or JWTOpenRouter'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-priceAPI key or JWTPaste-time price lookup for a free-text OpenRouter model id.
GET /api/agent-defaultsAPI key or JWTThe recommended starting stack for a new agent — the answer to "which providers should I put in my create body?".
GET /api/providers/tts/{p}/voicesnoneLive voice list. See above.
GET /api/providers/tts/{p}/modelsnoneLive model list. telenow only.
GET /api/providers/tts/elevenlabs/librarynoneThe ElevenLabs shared voice library.
POST /api/providers/tts/elevenlabs/library/addAPI key or JWTAdds a library voice to an account.
GET /api/providers/llm · /stt · /tts · /telephonynoneLegacy 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.

  • 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.

MethodPathAuth
GET/api/providers/llmpublic
GET/api/providers/sttpublic
GET/api/providers/ttspublic
GET/api/providers/telephonypublic
GET/api/providers/tts/{provider}/voicespublic
GET/api/providers/tts/{provider}/modelspublic
GET/api/providers/tts/{provider}/libraryneeds a key for that provider
POST/api/providers/tts/{provider}/library/addauthenticated
POST/api/providers/custom-endpoint/testauthenticated

/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.