$ curl api-enterprise-ai.teeoff.tech

API reference

Speech-to-text and text-to-speech over REST and WebSocket, with one auth scheme and one response envelope across both.

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 LE
AUTH

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

How audio crosses the wire

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 close
SERVER → 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 close
SERVER → 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 key
02Wait 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