The official TypeScript / JavaScript SDK for ClutchCall. Runs in Node, Bun, and modern browsers (the WASM core ships in the package).
ClutchCall is modality-oriented: every modality is its own subpath import, all riding the same MoQT substrate underneath. Pick the entry point that matches what you're building; you can mix them in one app.
| Subpath | Modality | Status |
|---|---|---|
clutchcall_connect/streams |
Live broadcasts + signed playback URLs | GA |
clutchcall_connect/robotics |
Robotics topic pub/sub (Zenoh-over-QUIC, ROS 2 CDR) | GA |
clutchcall_connect/games |
Games (rooms, state/input/event channels; Unity UPM drop-in) | GA |
clutchcall_connect/data |
MQTT-style typed pub/sub (topics + + / # filters, retained messages) |
GA |
clutchcall_connect/voice |
Voice (calls + bidirectional audio bridge + agent attach) | GA |
clutchcall_connect/moqt |
Realtime tracks (audio/video/frame) | GA |
clutchcall_connect/agent |
Agent-runtime control channel | GA |
clutchcall_connect (root) |
Legacy voice surface (ClutchCallClient) — kept for backwards compat |
legacy |
npm install clutchcall_connect
# or: pnpm add clutchcall_connect
# or: bun add clutchcall_connectThe Streams modality is the modality-first pattern. The control plane
(Streams) mints a short-lived playback URL; the data plane (BroadcastViewer)
opens it over WebTransport+MoQT and yields fMP4 chunks. The relay enforces the
playback JWT for you.
import { Streams, BroadcastViewer } from "clutchcall_connect/streams";
const streams = new Streams({
baseUrl: "https://app.clutchcall.dev",
apiKey: process.env.CLUTCHCALL_API_KEY!,
orgId: "org_abc",
});
const input = await streams.liveInputs.get({ id: "li_xyz" });
const { url, expiresAt } = await input.signedPlaybackUrl({ ttlSeconds: 3600 });
const viewer = await BroadcastViewer.open(url, {
onChunk: (init, chunk) => mediaSourceBuffer.appendBuffer(chunk.data),
onClose: (reason) => console.log("closed:", reason),
});A complete browser+MSE viewer is in
examples/streams_viewer.ts. The matching skill
for code generation is
clutchcall-streams.
Mirror of the viewer pattern for the publisher side. The cleartext stream
key is returned once, at create() / rotateStreamKey(); the BFF stores
only the hash, so save it.
import { Streams, BroadcastPublisher } from "clutchcall_connect/streams";
const streams = new Streams({ baseUrl: BFF, apiKey: KEY, orgId: ORG });
const { input, streamKey } = await streams.liveInputs.create({
name: "My Show",
});
const pub = await BroadcastPublisher.open({
inputId: input.external_input_id,
streamKey,
codecs: { video: "avc1.42E01F", audio: "opus" },
});
pub.write(fmp4Init); // CMAF init segment FIRST
pub.write(fmp4Segment); // media segments
await pub.close("finished");A browser-camera-to-broadcast example with MediaRecorder is in
examples/streams_publisher.ts.
The robotics modality enforces the bidirectional teleop convention so telemetry and commands never collide on the same namespace. Payload bytes are opaque CDR — your DDS / rmw_zenoh stack's serializer goes on the wire unchanged, prefixed with the ROS 2 type name for cross-language subscribers.
import { Robotics } from "clutchcall_connect/robotics";
const r = new Robotics({
relayHost: "relay.clutchcall.dev",
token: process.env.CLUTCHCALL_RELAY_TOKEN!,
robotId: "turtlebot-7",
});
// robot side — push odometry on robot/<id>
const odom = await r.publishTelemetry({
topic: "odom",
typeName: "nav_msgs/msg/Odometry",
qos: { reliability: "reliable", depth: 10 },
});
odom.write(cdrBytes);
// cloud side — push cmd_vel on robot/<id>/ctl
const cmd = await r.publishCommand({
topic: "cmd_vel",
typeName: "geometry_msgs/msg/Twist",
});
cmd.write(twistBytes);A robot-side publisher example is in
examples/robotics_telemetry.ts; a
cloud-side command publisher is in
examples/robotics_command.ts. The matching
skill is
clutchcall-robotics.
The games modality is one client per (room, player) — or (room, server)
for the authority — with three baked-in channels:
| Channel | Direction | Lane |
|---|---|---|
| state | server → all | datagram (lossy) |
| input | player → server | datagram (lossy) |
| event | any → any | stream (reliable) |
import { Games } from "clutchcall_connect/games";
// Player joining a room
const me = new Games({
relayHost: "relay.clutchcall.dev",
token: await fetchRoomToken("duel-42", "alice"),
roomId: "duel-42",
playerId: "alice",
});
me.subscribeState(bytes => render(deserializeState(bytes)));
const input = await me.publishInput();
addEventListener("tick", () => input.write(serializeInput(localInput)));// Authoritative server (no playerId)
const auth = new Games({ relayHost, token, roomId: "duel-42" });
const state = await auth.publishState({ tickHz: 30 });
await auth.subscribeInputs((pid, bytes) => applyInput(pid, deserializeInput(bytes)));
setInterval(() => state.write(serializeState(world)), 1000 / 30);A browser client example is in
examples/games_client.ts; a Node server loop
in examples/games_server.ts. For Unity
games, install the matching UPM package
com.clutchcall.transport
— a drop-in for com.unity.transport that speaks the same wire. The skill
is
clutchcall-games.
The "if you'd reach for MQTT, reach for this instead" modality.
Hierarchical topics, + / # wildcards, retained messages, all over the
same QUIC relay mesh — no broker. The SDK opens one MoQT track per
top-level topic segment and filters MQTT-style client-side.
import { Data } from "clutchcall_connect/data";
const data = new Data({
relayHost: "relay.clutchcall.dev",
token: process.env.CLUTCHCALL_DATA_TOKEN!,
clientId: "device-7",
});
// publish — lossy by default; flip reliable for application events
await data.publish({
topic: "sensors/room1/temperature",
payload: new TextEncoder().encode("23.5"),
});
// MQTT-style filter — + is one segment, # is the rest
await data.subscribe({ topicFilter: "sensors/+/temperature" }, (msg) => {
console.log(msg.topic, "←", msg.fromClientId, "=", new TextDecoder().decode(msg.payload));
});A device-side publisher (with retained boot state + alerts) is in
examples/data_publisher.ts; a fleet
dashboard with three MQTT filters is in
examples/data_dashboard.ts. The matching
skill is
clutchcall-data.
The modality-shaped voice surface. Two primitives baked in: Calls
(control plane) and AudioBridge (data plane), with the
voice/<sid>/{uplink,downlink} namespace convention enforced.
import { Voice } from "clutchcall_connect/voice";
const v = new Voice({ baseUrl: BFF, apiKey: KEY, orgId: ORG });
const call = await v.calls.originate({
to: "+15551234567",
from: "+15558675309",
trunkId: "trunk_main",
agent: "healthcare-assistant",
});
const bridge = await v.audioBridge.attach(call.sid, {
codec: "opus",
onUplink: (frame, tsUs) => myAsr.feed(frame),
});
myTts.onChunk(opus => bridge.publishDownlink(opus));
await call.hangup();A browser-mic outbound caller example is in
examples/voice_browser_caller.ts.
The matching skill is
clutchcall-voice.
The original ClutchCallClient + ClutchCallAudioStream (at the root
import) remains available for backwards compat. Set
CLUTCHCALL_CREDENTIALS to your service-account JSON, then:
import { ClutchCallClient } from "clutchcall_connect";
import { ClutchCallAudioStream } from "clutchcall_connect";
async function run() {
const client = new ClutchCallClient("https://pbx.clutchcall.com");
// Answer an inbound call and pipe its media into your bot.
await client.answerIncomingCall("call_sid_123", "wss://my-bot.com/media");
// Tap the raw audio if you want to do your own routing.
const stream = new ClutchCallAudioStream();
await stream.connect("wss://pbx.clutchcall.com/ai/session_789");
stream.onAudio((pcm: Uint8Array) => {
// Forward to OpenAI / Deepgram / your model of choice.
});
}
run();In Node, the SDK loads libclutchcall_core_ffi.{so,dylib,dll} via Node-API.
In the browser, it loads the WASM build (clutchcall_core_cc.wasm) shipped in
dist/. Build details: core-sdk.
clutchcall_connect/moqt is the first-class browser MoqtClient — the twin of
the native Python/Go/Rust/Java/.NET clients. It speaks MoQT directly over native
WebTransport (no FFI, no custom framing), so browser tracks interoperate with
the other SDKs and the relay's fan-out, and it auto-reconnects.
import { MoqtClient } from "clutchcall_connect/moqt";
const moqt = await MoqtClient.connect(
"https://relay.clutchcall.dev/moq", // relay runs on its own host, 443
token, // tenant token (optional in dev)
(state) => console.log("moqt state", state),
);
// Binary frame track (robot telemetry / game state), per-frame priority.
const pub = moqt.publishFrame("robot-7", "telemetry", { capability: "pose" });
pub.write(BigInt(Math.round(performance.now() * 1000)), encodePose(pose), /*priority*/ 0);
moqt.subscribeFrame("robot-7", "telemetry", (tsUs, priority, data) => render(data));publishAudio / subscribeAudio carry encoded audio; the helpers
captureMicrophone (mic → Opus) and OpusPlayer (WebCodecs playback) wire a
voice track end to end. onState: 0 Connecting · 1 Connected · 2 Reconnecting ·
3 Closed · 4 Failed.