JoinInDocs
Baton
v2 (beta)
Get a key
Baton v2 Get started Beta

Quickstart

Stream audio to Baton and print each turn end.

  1. Get a key

    Baton is in private beta. Request a key at hello@joinin.ai. Keys look like baton_ followed by 48 hexadecimal characters.
  2. Prepare audio

    16 000 Hz, mono, signed 16-bit little-endian PCM. Resample on your side; other rates are not accepted.
  3. Open a session and stream

    Send session.start, wait for session.started, then send binary frames: one byte 0, 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"}
  • t is audio time: seconds of audio you have sent, not wall-clock time.
  • seq starts at 1 and has no gaps.
  • vad.end is the end of speech sound; turn.end is Baton's decision that the speaker is done.

Next

© 2026 JoinIn AI, Inc. All rights reserved.joinin.ai