QUICKSTART
Your first call
Base URL https://api-enterprise-ai.teeoff.tech/api/enterprise/v1. Every operation below hangs off it.
01Get a key
Keys are minted in the developer console. The plaintext is shown once and never stored — lose it and you mint a new one.
X-Client-Id: ACME-PROD
X-API-Key: tea_live_…
02Synthesise speech
One synchronous call. It blocks for the whole synthesis, then returns a URL to a finished WAV.
curl -X POST https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/tts/speak \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Mhoro, ndinonzi Natasha.", "voice": "natasha"}'03Or stream it
Open a socket when audio has to start playing before synthesis finishes, or when you need barge-in.
const ws = new WebSocket("wss://api-enterprise-ai.teeoff.tech/api/enterprise/v1/ws/tts")
ws.onopen = () => ws.send(JSON.stringify({
type: "auth", client_id: CLIENT_ID, api_key: API_KEY
}))
// binary frames back are raw PCM, mono int16 LEAUTH
Authentication
Server-to-server calls authenticate with an API key sent as two headers. The key has the form <prefix>_<secret>; only the 12-character prefix is stored in the clear, and the secret is hashed with a per-key salt. Verification is constant-time, so an unknown client costs the same CPU as a valid one.
HEADERS
X-Client-Id: ACME-PROD
X-API-Key: tea_live_<22-char-suffix>
Browser code must never carry an API key. The console issues a short-lived JWT for that case, which the WebSocket routes accept as ?token=.
FAILURE MODES
401MISSING_API_KEYMissing header
401INVALID_API_KEYMalformed key, wrong secret, or key/client mismatch
401KEY_REVOKEDKey revoked
403CLIENT_SUSPENDEDClient suspended
403FEATURE_NOT_GRANTEDPlan does not grant the feature
429RATE_LIMITEDRate limit — token bucket empty
429 / 402QUOTA_EXCEEDEDMonthly quota exhausted
SHAPE
Response envelope
Every JSON response — success and error alike — arrives in the same wrapper. body carries the typed payload and is null on error; errors carries stable catalogue codes and is null on success. Write your client against the envelope once and every endpoint behaves the same.
{
"status": 200,
"message": "OK",
"errors": null,
"timestamp": 1712534400000,
"path": "/api/enterprise/v1/tts/speak",
"body": { "…": "T" },
"paginationDetails": null
}Every paginated route takes the same two query parameters — no cursor, no POST body. Page numbers are 0-indexed.
PAGINATIONDETAILS
{
"numberOfElements": 20,
"totalElements": 117,
"size": 20,
"pageNumber": 0,
"totalPages": 6,
"empty": false,
"first": true,
"last": false
}FORMATS
The one table worth reading before writing a client — the four routes carry media four different ways.
POST /tts/speakaudio out
Not in the response — body carries audio_url, a finished 16-bit PCM WAV served over CDN.
WS /ws/ttsaudio out
Binary frames are headerless raw PCM, mono int16 LE @ 24000 Hz. Text frames on the same socket are JSON status.
POST /stt/transcribeaudio in
multipart/form-data upload of a 16-bit PCM WAV. Response is JSON only.
WS /ws/sttaudio in
Client sends raw PCM, mono int16 LE @ 16000 Hz. Server sends only JSON.
- The two sockets negotiate different sample rates — 24 kHz down from TTS, 16 kHz up to STT. Piping one into the other needs a resample, and the session frame tells you what was actually negotiated.
- Streaming audio is never archived. Only POST /tts/speak produces a stored artifact; STT audio is never stored in either mode.
TEXT TO SPEECH
Text to speech
Feature code TTS, model teevoice/speak. Quota is measured in seconds of audio produced — hang up halfway and you pay for half.
POST/tts/speakSynthesise speech to a stored WAV
Synchronous — the call blocks for the whole synthesis, then returns a URL. The audio is not in the response body.
PARAMETERS
X-Client-Id *string · headerYour client code, e.g. ACME-PROD.
X-API-Key *string · headerThe plaintext key issued in the console.
REQUEST BODY · application/json
text *string1–5000 characters.
voicestringDefaults to natasha.
max_duration_secondsinteger1–600. Seconds, not tokens — the token budget is derived server-side, because that is a model internal a caller should not have to reason about.
{
"text": "Mhoro, ndinonzi Natasha.",
"voice": "natasha",
"max_duration_seconds": 60
}RESPONSES
200TtsSpeakBody. audio_url is an unauthenticated CDN URL with a uuid4 key. duration_seconds is exact; billed_seconds is that rounded up, and is what hit your quota.
{
"request_id": "01K1EXAMPLE",
"audio_url": "https://content-enterprise-ai.teeoff.tech/…wav",
"provider": "teevoice-speak",
"voice": "natasha",
"duration_seconds": 5.723,
"sample_rate": 24000,
"billed_seconds": 6
}400Validation failed — text empty or over 5000 characters, or max_duration_seconds out of range.
401Missing, malformed or revoked key.
MISSING_API_KEY · INVALID_API_KEY · KEY_REVOKED
403Client suspended, or the plan does not grant this feature.
CLIENT_SUSPENDED · FEATURE_NOT_GRANTED
429Token bucket empty, or the monthly quota is exhausted.
RATE_LIMITED · QUOTA_EXCEEDED
502UpstreamProviderError — the synthesiser errored or returned no audio. A history row is still written with status FAILED and an error_message.
UPSTREAM_UNAVAILABLE
EXAMPLE
curl -X POST https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/tts/speak \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Mhoro, ndinonzi Natasha.", "voice": "natasha"}'GET/tts/voicesList available voices
Currently one entry — TeeVoice Speak runs a fine-tuned checkpoint rather than a multi-voice model. It is an array so that adding a checkpoint later does not change the response shape.
PARAMETERS
X-Client-Id *string · headerYour client code, e.g. ACME-PROD.
X-API-Key *string · headerThe plaintext key issued in the console.
RESPONSES
200Array of voices.
[
{
"id": "natasha",
"name": "Natasha",
"language": "sn",
"description": "Shona female, fine-tuned accent (production default)"
}
]401Missing, malformed or revoked key.
MISSING_API_KEY · INVALID_API_KEY · KEY_REVOKED
403Client suspended, or the plan does not grant this feature.
CLIENT_SUSPENDED · FEATURE_NOT_GRANTED
429Token bucket empty, or the monthly quota is exhausted.
RATE_LIMITED · QUOTA_EXCEEDED
EXAMPLE
curl https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/tts/voices \
-H "X-Client-Id: $CLIENT_ID" -H "X-API-Key: $API_KEY"
WS/ws/ttsRealtime synthesis
Use this when audio must start playing before synthesis finishes, or when you need barge-in. Auth, the ready frame, timeouts and metering are shared — see WebSockets below.
PARAMETERS
client_idstring · queryAlternative to first-message auth. Machine clients only.
api_keystring · queryPairs with client_id.
tokenstring · queryConsole JWT — for browser code, which must never embed an API key.
REQUEST BODY · config frame (optional)
voicestringDefaults to natasha.
encodingstringpcm_s16le (default) or mulaw.
max_duration_secondsinteger1–600.
{"voice":"natasha","encoding":"pcm_s16le","max_duration_seconds":60}CLIENT → SERVER
{"type":"text","text":"Mhoro, ndinonzi Natasha."}
{"type":"flush"} // speak what is buffered now
{"type":"clear"} // barge-in: drop buffered and in-flight audio
{"type":"end"} // no more text; drain and closeSERVER → CLIENT
// binary frame -> audio: raw PCM, mono int16 LE @ negotiated sample_rate
{"type":"session","voice":"natasha","encoding":"pcm_s16le","sample_rate":24000}
{"type":"error","message":"…"}- Read sample_rate from the session frame rather than hardcoding 24000.
- Binary frames carry no WAV header and no framing. Concatenate to play; prepend a 44-byte header to save.
- Nothing is stored. Streaming sessions appear in history with the text and duration, but no audio_url.
- Unknown type values are ignored rather than fatal, so a forward-compatible client cannot kill its own stream.
- A word colliding with a raw sentinel (__FLUSH__, __CLEAR__, __END__) is dropped, not escaped — there is no escape mechanism, and a literal __CLEAR__ in prose would otherwise let a caller drive the stream.
GET/tts/historyList past synthesis requests
Paginated, newest first, scoped to the calling client.
PARAMETERS
X-Client-Id *string · headerYour client code, e.g. ACME-PROD.
X-API-Key *string · headerThe plaintext key issued in the console.
pageinteger · query0-indexed page number. Default 0, minimum 0.
sizeinteger · queryItems per page. 1–100.
RESPONSES
200Items carry request_id, status, transport (sync | WS), voice, text, audio_url, duration_seconds, error_message and created_at (epoch ms). audio_url is populated for /tts/speak rows and null for every WS row — streaming audio is not archived.
401Missing, malformed or revoked key.
MISSING_API_KEY · INVALID_API_KEY · KEY_REVOKED
403Client suspended, or the plan does not grant this feature.
CLIENT_SUSPENDED · FEATURE_NOT_GRANTED
429Token bucket empty, or the monthly quota is exhausted.
RATE_LIMITED · QUOTA_EXCEEDED
EXAMPLE
curl "https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/tts/history?page=0&size=20" \
-H "X-Client-Id: $CLIENT_ID" -H "X-API-Key: $API_KEY"
SPEECH TO TEXT
Speech to text
Feature code STT, model teevoice/listen. Quota is measured in seconds of audio submitted. Submitted audio is never persisted in either mode — the transcript is the deliverable, and the recording is usually of a third party.
POST/stt/transcribeTranscribe an uploaded file
Synchronous multipart upload. Mono or stereo, any sample rate — stereo is downmixed and anything not 16 kHz is resampled server-side.
PARAMETERS
X-Client-Id *string · headerYour client code, e.g. ACME-PROD.
X-API-Key *string · headerThe plaintext key issued in the console.
REQUEST BODY · multipart/form-data
file *binary16-bit PCM WAV. Max 25 MB.
languagestringDefaults to sn.
RESPONSES
200SttTranscribeBody. text is the committed transcript — the is_final segments joined in order. segments also carries interim hypotheses; ignore those unless rendering progress. start and end are seconds and may be null.
{
"request_id": "01K1EXAMPLE",
"text": "mhoro ndinonzi natasha uku kuedza kwetv",
"provider": "teevoice-listen",
"language": "sn",
"duration_seconds": 5.72,
"billed_seconds": 6,
"segments": [
{ "text": "mhoro ndinonzi natasha", "is_final": true,
"start": 0.0, "end": 2.1 }
]
}400Unreadable or empty file, or a format other than 16-bit PCM WAV. The format is deliberately narrow — no codec ships with the service, so an mp3 gets a 400 telling you what to send rather than a silently mangled transcript.
401Missing, malformed or revoked key.
MISSING_API_KEY · INVALID_API_KEY · KEY_REVOKED
403Client suspended, or the plan does not grant this feature.
CLIENT_SUSPENDED · FEATURE_NOT_GRANTED
429Token bucket empty, or the monthly quota is exhausted.
RATE_LIMITED · QUOTA_EXCEEDED
502The upstream recogniser errored.
UPSTREAM_UNAVAILABLE
EXAMPLE
# transcode first if needed:
# ffmpeg -i in.mp3 -ac 1 -ar 16000 -c:a pcm_s16le out.wav
curl -X POST https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/stt/transcribe \
-H "X-Client-Id: $CLIENT_ID" -H "X-API-Key: $API_KEY" \
-F "file=@speech.wav" -F "language=sn"
WS/ws/sttRealtime transcription
The client sends binary audio; the server sends only JSON — there is no binary downstream on this route.
PARAMETERS
client_idstring · queryAlternative to first-message auth.
api_keystring · queryPairs with client_id.
tokenstring · queryConsole JWT, for browser code.
REQUEST BODY · config frame (optional)
languagestringDefaults to sn.
sample_rateintegerDefaults to 16000.
{"language":"sn","sample_rate":16000}CLIENT → SERVER
// binary frames -> raw PCM, mono int16 LE @ 16000 Hz
{"type":"end"} // flush the final transcript and closeSERVER → CLIENT
{"type":"session","language":"sn","sample_rate":16000,"encoding":"pcm_s16le"}
{"type":"partial","text":"hongu natasha","start":null,"end":null}
{"type":"final","text":"mhoro ndinonzi natasha","start":0.0,"end":2.1}- Frame size is free, but ~20–100 ms per frame is the useful range: smaller wastes syscalls, larger adds latency to the utterance boundaries.
- A client that only wants committed text filters on type === "final". Partials will be revised.
- Submitted bytes are checked against the quota-derived cap as they arrive, so streaming faster than realtime cannot exceed your plan.
GET/stt/historyList past transcription requests
Paginated, newest first, scoped to the calling client.
PARAMETERS
X-Client-Id *string · headerYour client code, e.g. ACME-PROD.
X-API-Key *string · headerThe plaintext key issued in the console.
pageinteger · query0-indexed page number. Default 0, minimum 0.
sizeinteger · queryItems per page. 1–100.
RESPONSES
200Items carry request_id, status, transport (sync | WS), language, text, duration_seconds, utterances, error_message and created_at (epoch ms). There is no audio URL on either transport.
401Missing, malformed or revoked key.
MISSING_API_KEY · INVALID_API_KEY · KEY_REVOKED
403Client suspended, or the plan does not grant this feature.
CLIENT_SUSPENDED · FEATURE_NOT_GRANTED
429Token bucket empty, or the monthly quota is exhausted.
RATE_LIMITED · QUOTA_EXCEEDED
EXAMPLE
curl "https://api-enterprise-ai.teeoff.tech/api/enterprise/v1/stt/history?page=0&size=20" \
-H "X-Client-Id: $CLIENT_ID" -H "X-API-Key: $API_KEY"
REALTIME
WebSocket admission
Both sockets share one admission sequence; only the config and data frames differ. Neither route appears in the OpenAPI document — OpenAPI cannot describe them — so this is their specification.
01Authenticate
First-message auth is preferred: nothing sensitive touches the URL, so keys stay out of load-balancer access logs, proxies and browser history.
{"type": "auth", "client_id": "acme", "api_key": "tea_live_…"}
// or, as a query string
?client_id=…&api_key=… machine clients
?token=<console JWT> browser code, which must never embed an API key02Wait for ready
Sent once auth, quota and the connection rate limit all pass.
{"type":"ready","request_id":"01K1…","feature":"TTS",
"max_session_seconds":900,"quota_remaining_seconds":1794}03Optionally configure, then read the session frame
Config is distinguished from a control frame by the absence of a type key. Every field has a server-side default, so the frame is optional — a client that goes straight to sending data is fine, and its first frame is not eaten.
// client → server (optional)
{"voice":"natasha","encoding":"pcm_s16le","max_duration_seconds":60}
// server → client, confirming what was negotiated
{"type":"session","voice":"natasha","encoding":"pcm_s16le","sample_rate":24000}- Idle timeout 120 s with no input; hard session cap 900 s. Either sends a notice frame before closing.
- max_session_seconds is the lesser of the configured cap and your remaining monthly quota — quota is checked once at connect, so it acts as a limit rather than a gate.
- A clean end closes 1000; admission failures close 1008 — but the structured error arrives as a normal frame first, because browsers cannot read a close reason reliably.
- Metering is per connection, not per frame: one usage row is written at close. Audio arrives at roughly 50 frames a second, and a rate-limit round-trip per frame would cost more than the inference.
ERROR FRAME
{"type":"error","status":402,"code":"quota_exceeded","message":"…"}status mirrors the HTTP status the same failure would have produced — 401, 402, 403, 429, 500 — so telemetry stays comparable across transports.
This covers the voice API. The full reference — image, avatar and video generation, webhooks, usage and the model catalogue — lives in the developer console.Get API key