Get started / Authentication
Authentication
The API authenticates with an Authorization: Bearer
token. Auth is configured on the box, so a public deployment requires a key
while an embedded loopback box can run open.
API keys
Set one or more keys on the server and send it on every /v1 request:
- Single key —
ZELI_API_KEY=sk-zeli-... - Multiple keys —
ZELI_API_KEYS=sk-zeli-a,sk-zeli-b(comma-separated)
If neither variable is set, the API is open — correct for a loopback/embedded box, wrong for a public one. Always set a key before exposing the box to a network.
Sending the key
Send the key as an Authorization: Bearer header.
curl -X POST "https://voice.zeligate.com/v1/text-to-speech/zeli-voice-1" \
-H "Authorization: Bearer sk-zeli-..." \
-H "Content-Type: application/json" \
-d '{"text":"Authenticated with a Bearer token."}' --output out.mp3from zeli_tts import ZeliSpeech
# The SDK sends the key automatically.
client = ZeliSpeech(api_key="sk-zeli-...", base_url="https://voice.zeligate.com")WebSocket authentication
The realtime stream-input WebSocket reads the key from either of two places:
- The BOS message field
authorization(aBearer sk-zeli-...token). - A
?authorization=Bearer%20sk-zeli-...query parameter (for browser clients that can't set headers).
{ "text": " ", "authorization": "Bearer sk-zeli-...", "voice_settings": { "stability": 0.5 } }See Realtime WebSocket for the full protocol.
Errors
Auth failures use the ZeliSpeech error envelope:
{ "detail": { "status": "missing_api_key", "message": "..." } }| Status code | status | When |
|---|---|---|
401 | missing_api_key | Auth is required but no key was sent |
401 | invalid_api_key | The key sent isn't recognized (or has expired) |
401 | insufficient_scope | An ephemeral token was used on a management route |
See Errors for the complete list.
Ephemeral tokens (browser audio)
Never ship a real zsk_live_ key to a browser. Instead, your backend mints a
short-lived, synthesis-only token and hands it to the page; the browser then
streams audio directly from the API until the token expires.
POST /v1/tokens/ephemeral (requires a FULL API key)
{ "ttl_seconds": 300 } // clamped to 30–600, default 300
→ { "token": "zsk_temp_…", "expires_at": 1770000000, "scope": "tts" }Ephemeral tokens can call synthesis and read endpoints (/v1/text-to-speech/*,
GET /v1/voices*, the realtime WebSocket) but not voice cloning/removal or
token minting — those return 401 insufficient_scope. Expiry is enforced
server-side on every request.
Backend (Node) → browser flow:
// backend route — holds the real key
import { ZeliSpeech } from "zelispeech";
const zeli = new ZeliSpeech({ baseUrl: BASE, apiKey: process.env.ZELISPEECH_API_KEY });
app.post("/api/tts-token", async (req, res) => res.json(await zeli.tokens.createEphemeral()));
// browser — uses only the temp token
const { token } = await (await fetch("/api/tts-token", { method: "POST" })).json();
const zeliClient = new ZeliSpeech({ baseUrl: BASE, apiKey: token });
const clip = await zeliClient.textToSpeech.convert("zeli-voice-1", "Hello!");
new Audio(URL.createObjectURL(new Blob([clip], { type: "audio/mpeg" }))).play();Ephemeral tokens require the portal keys table on the box
(ZELI_API_KEYS_TABLE); a static-keys-only box returns 501 not_supported.
Provisioning a public box
- Set
ZELI_API_KEY(orZELI_API_KEYS) on the box. - Terminate TLS in front — clients expect
https://. - Optionally pin the swap-out voice with
ZELI_EL_DEFAULT_VOICE=zeli-voice-1.
Per-key concurrency limits, queueing, and 429 rate limiting are on the roadmap
(the engine is batch-1, so concurrency control matters for public boxes).