-
Get a key
Baton is in private beta. Request a key at hello@joinin.ai. Keys look likebaton_followed by 48 hexadecimal characters. -
Prepare audio
16 000 Hz, mono, signed 16-bit little-endian PCM. Resample on your side; other rates are not accepted. -
Open a session and stream
Sendsession.start, wait forsession.started, then send binary frames: one byte0, then the PCM samples.
Python
import asyncio, json, wave
import websockets # pip install websockets
URL = "wss://baton-test.joinin.ai/v2/stream"
KEY = "baton_..."
async def main(path):
async with websockets.connect(URL, max_size=None) as ws:
await ws.send(json.dumps({"type": "session.start", "api_key": KEY,
"eot": {"profile": "cutoff_5pct"}}))
started = json.loads(await ws.recv())
print("profile", started["eot"]["profile"])
async def send_audio():
with wave.open(path) as w: # 16 kHz mono 16-bit
while frame := w.readframes(1600): # 100 ms
await ws.send(b"\x00" + frame) # source byte, then PCM
await asyncio.sleep(0.1) # real time
await ws.send(json.dumps({"type": "session.end"}))
sender = asyncio.create_task(send_audio())
async for raw in ws:
event = json.loads(raw)
if event["type"] == "turn.end":
print(f"turn ended at {event['t']} s after {event['latency_ms']} ms")
elif event["type"] == "session.summary":
print(event)
await sender
asyncio.run(main("call.wav"))
JavaScript
// Node 22+ (global WebSocket). pcm: Int16Array of 16 kHz mono samples.
const ws = new WebSocket("wss://baton-test.joinin.ai/v2/stream");
ws.binaryType = "arraybuffer";
ws.onopen = () => ws.send(JSON.stringify({
type: "session.start", api_key: "baton_...", eot: { profile: "cutoff_5pct" },
}));
ws.onmessage = async ({ data }) => {
const event = JSON.parse(data);
if (event.type === "session.started") {
for (let i = 0; i < pcm.length; i += 1600) { // 100 ms frames
const chunk = pcm.subarray(i, i + 1600);
const frame = new Uint8Array(1 + chunk.byteLength); // [source=0][PCM16LE]
frame.set(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), 1);
ws.send(frame);
await new Promise(r => setTimeout(r, 100)); // real time
}
ws.send(JSON.stringify({ type: "session.end" }));
}
if (event.type === "turn.end") console.log(`turn ended at ${event.t} s`);
};
What you receive
{"type": "vad.start", "seq": 1, "schema_version": 2,
"t": 0.1, "speaker": "user"}
{"type": "vad.end", "seq": 2, "schema_version": 2, "t": 2.3,
"speaker": "user", "duration": 2.2}
{"type": "turn.end", "seq": 3, "schema_version": 2,
"t": 3.0, "speaker": "user", "p_eot": 0.81,
"latency_ms": 700, "by": "score", "profile": "cutoff_5pct"}
tis audio time: seconds of audio you have sent, not wall-clock time.seqstarts at 1 and has no gaps.vad.endis the end of speech sound;turn.endis Baton's decision that the speaker is done.