C-elo API v1

Stream African speech into text

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.

Keys shown once

Save each API key when you create it; it cannot be viewed again.

One-use sessions

Session credentials expire after 60 seconds and cannot be replayed.

Audio-second credits

Usage is metered from accepted PCM audio, not transcript characters or LLM tokens.

No audio retention

The API stores usage metadata, not audio or transcript content, by default.

Quickstart

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.

1. Reserve a streaming session

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.

2. Connect from Node.js

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");
  }
});

Python

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())

Audio format

Signed 16-bit little-endian PCM (pcm_s16le)
16,000 Hz sample rate
One mono channel
Binary WebSocket frames, at most 65,536 bytes each
Even byte count; 32,000 bytes equals one second

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.

WebSocket protocol

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.

Browser and mobile integrations

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.

Credits and limits

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.

  • Maximum 300 audio seconds per session.
  • One concurrent session per developer account initially.
  • Six session reservations per minute per project initially.
  • Check GET /v1/usage for available, reserved, and consumed milliseconds.

Errors and retries

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": "..."
  }
}

Security checklist

  • Store permanent keys in a secret manager, never source control.
  • Do not log Authorization headers or session responses.
  • Use one key per environment or service and revoke unused keys.
  • Never call C-elo with a permanent key from browser code.
  • Use HTTPS/WSS only and close streams promptly.

Machine-readable contracts

The HTTP API is defined in OpenAPI 3.1 and the streaming protocol in AsyncAPI 3.1. These files are the source of truth.