ClutchCall.SDK namespace.
using ClutchCall.SDK;
// Voice, Streams, Robotics, Games, Data, Moqt
IAsyncDisposable.
Voice
var v = new Voice(
baseUrl: "https://app.clutchcall.dev",
apiKey: Environment.GetEnvironmentVariable("CLUTCHCALL_API_KEY"),
orgId: "org_abc");
Calls
var call = await v.Calls.OriginateAsync(new Voice.OriginateArgs {
To = "+15551234567",
From = "+15558675309",
TrunkId = "trunk_main",
Agent = "healthcare-assistant",
});
await v.Calls.GetAsync(sid);
await v.Calls.ListAsync(orgId, cursor, limit);
await v.Calls.TransferAsync(sid, new Voice.TransferArgs { To = "+1..." });
await v.Calls.HangupAsync(sid);
await v.Calls.TerminateAsync(sid);
Call:
call.Sid : string
call.Status : string
call.From : string
call.To : string
await call.RefreshAsync();
await call.HangupAsync();
await call.TransferAsync(args);
call.OnStatus(action);
AudioBridge
var bridge = await v.AudioBridge.AttachAsync(call.Sid,
new Voice.AudioBridgeOpts {
Codec = Voice.Codec.Opus,
OnUplink = (frame, tsUs) => myAsr.Feed(frame),
});
bridge.PublishUplink(frame);
bridge.PublishDownlink(frame);
bridge.OnUplink(cb);
bridge.OnDownlink(cb);
await bridge.DisposeAsync();
Codec: Opus, Pcm16, G711ULaw, G711ALaw.
Streams
var s = new Streams("https://app.clutchcall.dev", apiKey, "org_abc");
Control plane
var created = await s.LiveInputs.CreateAsync(new Streams.CreateLiveInput {
Name = "My Show",
});
string streamKey = created.StreamKey; // returned ONCE
await s.LiveInputs.GetAsync(id);
await s.LiveInputs.ListAsync(orgId, cursor, limit);
await s.LiveInputs.RotateStreamKeyAsync(id);
await s.LiveInputs.DeleteAsync(id);
await s.SigningKeys.CreateAsync(orgId, label);
await s.SigningKeys.ListAsync(orgId);
await s.SigningKeys.RetireAsync(id);
await s.ApiKeys.CreateAsync(orgId, label, scopes);
await s.ApiKeys.ListAsync(orgId);
await s.ApiKeys.RevokeAsync(id);
await s.Webhooks.CreateAsync(orgId, url, events);
await s.Webhooks.ListAsync(orgId);
await s.Webhooks.DeleteAsync(id);
await s.Events.ListDeliveriesAsync(webhookId, cursor, limit);
await s.Analytics.ViewerMinutesAsync(orgId, from, to, groupBy);
await s.Analytics.PopCacheAsync(orgId, from, to);
LiveInput:
input.ExternalInputId : string
await input.SignedPlaybackUrlAsync(ttlSeconds, scopes); // → SignedPlaybackUrl
Data plane
var pub = await Streams.BroadcastPublisher.OpenAsync(new Streams.PublisherArgs {
InputId = input.ExternalInputId, StreamKey = streamKey,
Codecs = new Streams.Codecs { Video = "avc1.42E01F", Audio = "opus" },
});
pub.Write(chunk);
await pub.CloseAsync(Streams.CloseReason.Complete);
var viewer = await Streams.BroadcastViewer.OpenAsync(signedUrl, new Streams.ViewerOpts {
OnChunk = (init, chunk) => { /* ... */ },
OnClose = (reason) => { /* ... */ },
});
await viewer.DisposeAsync();
Robotics
var r = new Robotics(
relayHost: "relay.clutchcall.dev",
token: Environment.GetEnvironmentVariable("CLUTCHCALL_RELAY_TOKEN"),
robotId: "turtlebot-7");
var qos = new Robotics.QoSProfile {
Reliability = Robotics.Reliability.Reliable,
Depth = 10,
};
var pub = await r.PublishTelemetryAsync("odom",
"nav_msgs/msg/Odometry", qos);
pub.Write(cdrBytes);
await pub.DisposeAsync();
var cmd = await r.PublishCommandAsync("cmd_vel",
"geometry_msgs/msg/Twist", null);
var sub = await r.SubscribeTelemetryAsync("odom", null,
(payload, typeName) => { /* ... */ });
await sub.DisposeAsync();
Reliability: BestEffort, Reliable.
Durability: Volatile, TransientLocal.
Games
var g = new Games(
relayHost: "relay.clutchcall.dev",
token: token,
roomId: "duel-42",
playerId: "alice"); // null for authority
Player
var stateSub = g.SubscribeState(bytes => Render(bytes));
var input = await g.PublishInputAsync();
input.Write(SerializeInput(localInput));
var events = await g.PublishEventAsync();
events.Send(new { kind = "chat", text = "gg" });
g.SubscribeEvents((evt, fromPlayerId) => Dispatch(evt));
Authority (playerId: null)
var state = await g.PublishStateAsync(new Games.StateOpts { TickHz = 30 });
state.Write(SerializeState(world));
g.SubscribeInputs((playerId, payload) => ApplyInput(playerId, payload));
g.SubscribeEvents((evt, fromPlayerId) => { /* ... */ });
Data
var d = new Data(
relayHost: "relay.clutchcall.dev",
token: Environment.GetEnvironmentVariable("CLUTCHCALL_DATA_TOKEN"),
clientId: "device-7");
await d.PublishAsync(
topic: "sensors/room1/temperature",
payload: Encoding.UTF8.GetBytes("23.5"),
reliable: false,
retain: false);
var sub = await d.SubscribeAsync("sensors/+/temperature",
msg => Console.WriteLine($"{msg.Topic} ← {msg.FromClientId} = " +
$"{Encoding.UTF8.GetString(msg.Payload)}"));
await sub.DisposeAsync();
Data.Message:
msg.Topic : string
msg.Payload : byte[]
msg.FromClientId : string
msg.Retain : bool
msg.TsUs : ulong
Realtime Tracks
See Realtime Tracks. Common methods:var client = await Moqt.Client.ConnectAsync("quic://relay.clutchcall.dev", token);
await client.PublishFrameAsync(ns, name, opts);
client.SubscribeFrame(ns, name, (tsUs, prio, data) => {...});
client.PublishAudio(...) / client.SubscribeAudio(...)
client.PublishVideo(...) / client.SubscribeVideo(...)
client.PublishText(...) / client.SubscribeText(...)
client.SubscribeNamespace(prefix, (suffix, active) => {...});
client.ConnectionRttUs : ulong
client.MaxDatagramSize : int
await client.DisposeAsync();
Legacy RPC
The rootClutchCall.SDK.ClutchCallClient (Dial, OriginateBulk,
Hangup, Barge, PushAudio, …) remains for backwards compat. New code
should prefer the voice modality.
