Keys shown once
Save each API key when you create it; it cannot be viewed again.
Use C-elo's Kikuyu and Dholuo streaming speech-to-text API in two steps: create a short-lived session with your server-side API key, then stream mono 16 kHz PCM audio over WSS using the returned connection details.
Save each API key when you create it; it cannot be viewed again.
Session credentials expire after 60 seconds and cannot be replayed.
Usage is metered from accepted PCM audio, not transcript characters or LLM tokens.
The API stores usage metadata, not audio or transcript content, by default.
Join the developer waitlist with the same email you use to sign in. After C-elo approves access, create a key in the developer console and save it in a server secret manager as CELO_API_KEY. It is shown only once.
curl https://api.c-elo.com/v1/asr/sessions \
--request POST \
--header "Authorization: Bearer $CELO_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: session-$(uuidgen)" \
--data '{
"model": "celo-asr-kikuyu-v1",
"max_audio_seconds": 60
}'The response contains websocket_url and two websocket_protocols. The second value contains the one-use session credential; do not log the response.
import fs from "node:fs";
import WebSocket from "ws";
const sessionResponse = await fetch(
"https://api.c-elo.com/v1/asr/sessions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CELO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
model: "celo-asr-kikuyu-v1",
max_audio_seconds: 60,
}),
},
);
if (!sessionResponse.ok) {
throw new Error(await sessionResponse.text());
}
const session = await sessionResponse.json();
const ws = new WebSocket(
session.websocket_url,
session.websocket_protocols,
);
let audioStarted = false;
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "ready" && !audioStarted) {
audioStarted = true;
ws.send(JSON.stringify({
type: "config",
}));
const audio = fs.createReadStream("speech.pcm", {
highWaterMark: 32_000,
});
audio.on("data", (chunk) => ws.send(chunk));
audio.on("end", () => ws.send(JSON.stringify({ type: "flush" })));
}
if (event.type === "delta" || event.type === "final") {
console.log(event.text);
}
if (event.type === "final") {
ws.close(1000, "complete");
}
});import asyncio
import json
import os
import uuid
import httpx
import websockets
async def transcribe():
async with httpx.AsyncClient(timeout=15) as client:
response = await client.post(
"https://api.c-elo.com/v1/asr/sessions",
headers={
"Authorization": f"Bearer {os.environ['CELO_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"model": "celo-asr-dholuo-v1",
"max_audio_seconds": 60,
},
)
response.raise_for_status()
session = response.json()
async with websockets.connect(
session["websocket_url"],
subprotocols=session["websocket_protocols"],
) as ws:
async for raw_event in ws:
event = json.loads(raw_event)
if event["type"] == "status":
print(event["message"])
if event["type"] == "ready":
break
await ws.send(json.dumps({
"type": "config",
}))
with open("speech.pcm", "rb") as audio:
while chunk := audio.read(32_000):
await ws.send(chunk)
await ws.send(json.dumps({"type": "flush"}))
async for raw_event in ws:
event = json.loads(raw_event)
if event["type"] in {"delta", "final"}:
print(event["text"])
if event["type"] == "final":
break
asyncio.run(transcribe())Convert compressed audio on your server before streaming. For example: ffmpeg -i speech.wav -f s16le -acodec pcm_s16le -ac 1 -ar 16000 speech.pcm.
The service may send status updates while the session initializes. Wait for ready before sending binary audio, then listen for JSON delta and final events. A flush frame finalizes buffered audio; reset starts a fresh utterance in the same session.
{"type":"status","message":"Preparing stream"}
{"type":"ready","chunk_samples":...}
{"type":"delta","text":"..."}
{"type":"final","text":"..."}
{"type":"error","message":"..."}Send a config frame containing only type to use the service defaults. See the AsyncAPI 3.1 document for the machine-readable message contract.
A permanent key grants credits and must remain on your trusted server. Your server creates the C-elo session and relays only the short-lived WebSocket response to the client.
// Your trusted server creates the session with CELO_API_KEY.
const session = await fetch("/your-api/celo-session", {
method: "POST",
}).then((response) => response.json());
// The browser receives only the 60-second, one-use credential.
const ws = new WebSocket(
session.websocket_url,
session.websocket_protocols,
);
// Never place CELO_API_KEY in browser, mobile, or desktop client code.Initial credits are audio time, not tokens. Session creation reserves up to the requested maximum. If at least one second remains but less credit is available than requested, the response may reserve a shorter session; use audio.max_audio_seconds and credits.reserved as the actual limit. Unused time is returned when the session ends. Accepted audio is rounded up to the next second. Service failures are not charged.
GET /v1/usage for available, reserved, and consumed milliseconds.Every HTTP error includes a stable code and request_id. Retry 429 and 503 responses using the Retry-After header, exponential backoff, and the same Idempotency-Key.
{
"error": {
"type": "rate_limit_error",
"code": "concurrency_limit_exceeded",
"message": "This account already has an active or reserved ASR session.",
"request_id": "..."
}
}The HTTP API is defined in OpenAPI 3.1 and the streaming protocol in AsyncAPI 3.1. These files are the source of truth.