Avatar Service REST API
Unlike the rest of the platform, avatar/voice operations are exposed over a separate REST service, not the GraphQL API at api.wetel.dev/graphql. This service handles text-to-speech (TTS) synthesis, speech-to-text (STT) transcription, and serving the catalog of available 3D avatar models. Requests go to your assigned avatar-service endpoint (referred to below as https://avatar.wetel.dev — confirm the exact hostname for your environment with the platform team).
This page assumes you’re already familiar with Avatar Config and Sessions from the main GraphQL API. See also <vai-avatar> and Voice Interface for the higher-level client components that call these endpoints for you — most integrations should reach for those rather than calling this REST API directly.
Authentication
Section titled “Authentication”Every endpoint below (except the public model catalog and health check) requires an avatar token — a short-lived, HMAC-signed token that is distinct from your regular JWT or API key. Send it as a standard bearer token:
Authorization: Bearer <AVATAR_TOKEN>You obtain an avatar token from the main GraphQL API, not from this service directly:
- the
avatarTokenquery (see Avatar Config), or - automatically, as part of
sdkStart’s response, when using the embed SDK entry point.
Rate limiting: two independent ceilings per route
Section titled “Rate limiting: two independent ceilings per route”Added 2026-09-17. Every authenticated route below (/tts, /tts/google, /stt, /avatar-session) is governed by two separate, named throttles that must both allow the request:
- A per-client-IP ceiling (the number quoted in each route’s own “Rate limit” line below) — this existed before 2026-09-17 and is unchanged.
- A per-tenant ceiling, new as of 2026-09-17, shared across every caller currently authenticated with that tenant’s avatar tokens (regardless of which IP each caller connects from) — set to 20x the per-IP figure on every route.
A request without a valid, current avatarToken (missing header, malformed token, expired token, bad signature) is governed by the per-IP ceiling only — the per-tenant throttle is skipped entirely for it, not silently pooled into a shared “no tenant” bucket. Exceeding either ceiling returns the same 429-equivalent throttling response; there’s no way from the response alone to tell which of the two was hit.
POST /tts
Section titled “POST /tts”Synthesizes speech from text or SSML.
Auth required: Avatar token (Bearer).
Rate limit: 30 requests/minute per client IP, and 600 requests/minute per tenant (added 2026-09-17 — see Rate limiting: two independent ceilings per route above).
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | One of text/ssml | Plain text to synthesize |
ssml | string | One of text/ssml | SSML markup; takes precedence over text if both are set |
voiceName | string | Yes | |
languageCode | string | Yes | e.g. en-US |
speakingRate | number | No | 0.25–4.0 |
audioEncoding | string | No | MP3 or LINEAR16 |
Response: 200 OK
| Field | Type | Notes |
|---|---|---|
audioContent | string | Base64-encoded audio |
timepoints | array | [{ markName: string, timeSeconds: number }] — SSML mark timing, for lip-sync |
curl -X POST https://avatar.wetel.dev/tts \ -H "Authorization: Bearer <AVATAR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "text": "Hello, how can I help you today?", "voiceName": "en-US-Neural2-F", "languageCode": "en-US" }'{ "audioContent": "//uQxAAAAAAAAAAAAAAAAAAAAAAAAAAA...", "timepoints": []}POST /tts/google
Section titled “POST /tts/google”A request/response-shape-compatible variant of /tts, for third-party avatar rendering libraries (such as TalkingHead) that expect the Google Cloud Text-to-Speech request format instead of this service’s native shape.
Auth required: Avatar token (Bearer).
Rate limit: 30 requests/minute per client IP, and 600 requests/minute per tenant (added 2026-09-17 — see Rate limiting: two independent ceilings per route above).
Request body (Google Cloud TTS shape):
{ "input": { "text": "Hello there", "ssml": null }, "voice": { "languageCode": "en-US", "name": "en-US-Neural2-F" }, "audioConfig": { "speakingRate": 1.0 }}Only input.text/input.ssml, voice.name, voice.languageCode, and audioConfig.speakingRate are read — any other fields a third-party library sends alongside these (e.g. enableTimePointing) are accepted and ignored, not rejected.
Response: Same shape as /tts — { audioContent: string, timepoints: [...] }.
POST /stt
Section titled “POST /stt”Transcribes an audio clip to text.
Auth required: Avatar token (Bearer).
Rate limit: 10 requests/minute per client IP — lower than TTS because STT is billed per 15-second audio block upstream, so this limit is intentionally more conservative — and 200 requests/minute per tenant (added 2026-09-17 — see Rate limiting: two independent ceilings per route above).
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
audioBase64 | string | Yes | Base64-encoded audio, max ~4MB of raw audio (~5,600,000 base64 characters) |
audioEncoding | string | Yes | MP3, LINEAR16, WEBM_OPUS, or OGG_OPUS |
languageCode | string | Yes | e.g. en-US |
sampleRateHertz | number | No | 8000–48000. If omitted: 48000 for WEBM_OPUS, 16000 for every other encoding. |
OGG_OPUS (OGG container, Opus codec) is the native format WhatsApp and Telegram voice notes are recorded in — send one straight through without transcoding it first. It’s a distinct value from WEBM_OPUS because the two use different container formats despite both carrying Opus audio.
Response: 200 OK
| Field | Type | Notes |
|---|---|---|
transcript | string | |
confidence | number | 0–1 |
languageCode | string |
curl -X POST https://avatar.wetel.dev/stt \ -H "Authorization: Bearer <AVATAR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "audioBase64": "UklGRi...", "audioEncoding": "WEBM_OPUS", "languageCode": "en-US" }'{ "transcript": "What time does the library close today?", "confidence": 0.94, "languageCode": "en-US"}If the underlying speech provider is rate-limited or unavailable, this endpoint returns 503 Service Unavailable rather than a generic 500 — worth handling as a distinct, retryable case in your client.
This endpoint uses synchronous transcription, which has a practical ceiling of roughly 60 seconds of audio per call. A typical voice note or conversational turn is well under that; if you need to transcribe something longer, split it into chunks client-side rather than sending one large file.
POST /avatar-session
Section titled “POST /avatar-session”Negotiates a HOSTED_API (photorealistic video avatar) rendering session. Only relevant if your agent’s avatarBackend is configured to HOSTED_API — see Avatar & 3D Rendering: Avatar rendering options. Not used for the default 3D (CLIENT_3D) rendering path, which is driven entirely by sdkStart’s response instead.
Auth required: Avatar token (Bearer).
Rate limit: 10 requests/minute per client IP, and 200 requests/minute per tenant (added 2026-09-17 — see Rate limiting: two independent ceilings per route above). Lower than TTS/STT because this negotiates a real, per-minute-billed hosted video session with the underlying vendor, not a per-utterance call.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
anamAvatarId | string | No | Must match the value already configured on the agent’s avatarBackend config — sent only so a mismatch can be rejected loudly. |
anamAvatarModel | string | No | Same as above. |
Your own client code never chooses or overrides the vendor persona/model here — the real value comes from the agent’s server-side avatarBackendConfig, embedded in the avatarToken at sdkStart time. These fields exist only so the server can detect and reject a mismatch, not so the browser can request an arbitrary persona.
Response: 200 OK
| Field | Type | Notes |
|---|---|---|
sessionToken | string | A vendor session credential. The browser never sees the vendor’s own API key. |
curl -X POST https://avatar.wetel.dev/avatar-session \ -H "Authorization: Bearer <AVATAR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{}'{ "sessionToken": "eyJhbGciOi..."}A negotiation failure (missing vendor credentials, vendor outage) returns a generic 500 with no vendor-specific detail — the underlying error is logged server-side, never surfaced to the browser, since it may reference internal config state.
GET /avatars/models
Section titled “GET /avatars/models”Returns the catalog of available 3D avatar models.
Auth required: None — this is a public catalog listing.
Response: 200 OK
{ "models": [ { "id": "avatar-01", "label": "Aria", "gender": "female", "path": "/avatars/models/aria.glb" } ]}| Field | Type | Notes |
|---|---|---|
id | string | |
label | string | Display name |
gender | string | |
path | string | Relative path to the .glb model asset |
curl https://avatar.wetel.dev/avatars/modelsGET /health
Section titled “GET /health”A basic health check endpoint — returns a 200 OK when the service is up. No auth required, no meaningful response body to parse; use it only for uptime/liveness checks, not as a functional API.
For the config that determines which voice/avatar a given agent uses by default, see Avatar Config. For how sessions and avatar tokens fit together, see Sessions. For the client-side components that wrap this API, see <vai-avatar> and Voice Interface.