Control plane
Discover hubs and create client identities.
Use this page when you want to build with the SDK, not memorize every method name.
Start with the quick example, then copy the recipe closest to your task. Each recipe shows the same idea in Python, Node.js, Go, and Rust where the SDKs expose it.
This example signs in, creates a client identity for one hub, connects, sends one request, and prints the reply.
from thalovant import ThalovantClient, ThalovantControlPlane
api = ThalovantControlPlane()
result = api.create_client_identity( "hub-id", name="python-demo-client", preferred_protocols=("wss", "https", "mqtt"),)
with ThalovantClient(result.identity, protocol="wss") as client: reply = client.ask("Tell me a short clean joke.") print(reply.text)import { ThalovantClient, ThalovantControlPlane } from "@thalovant/sdk";
const api = new ThalovantControlPlane();
const result = await api.createClientIdentity("hub-id", { name: "node-demo-client", preferredProtocols: ["wss", "https", "mqtt"],});
const client = new ThalovantClient(result.identity, { protocol: "wss" });try { const reply = await client.ask("Tell me a short clean joke."); console.log(reply.text);} finally { await client.close();}package main
import ( "context" "fmt" "log"
thalovant "github.com/thalovant/thalovant-go-sdk")
func main() { ctx := context.Background() api := thalovant.NewDefaultControlPlane("")
log.Fatal(err) }
result, err := api.CreateClientIdentityForHubID(ctx, "hub-id", thalovant.BootstrapIdentityOptions{ Name: "go-demo-client", PreferredProtocols: []thalovant.HubProtocol{thalovant.ProtocolWSS, thalovant.ProtocolHTTPS, thalovant.ProtocolMQTT}, }) if err != nil { log.Fatal(err) }
client, err := thalovant.NewClientWithOptions(result.Identity, thalovant.ClientOptions{Protocol: thalovant.ProtocolWSS}) if err != nil { log.Fatal(err) } defer client.Close(ctx)
reply, err := client.Ask(ctx, "Tell me a short clean joke.", thalovant.RequestOptions{}) if err != nil { log.Fatal(err) } fmt.Println(reply.Text)}use thalovant::{BootstrapIdentityOptions, Client, ControlPlane, HubProtocol, RequestOptions};
#[tokio::main]async fn main() -> thalovant::Result<()> { let mut api = ControlPlane::default();
let result = api .create_client_identity_for_hub_id( "hub-id", BootstrapIdentityOptions { name: "rust-demo-client".into(), preferred_protocols: vec![HubProtocol::Wss, HubProtocol::Https, HubProtocol::Mqtt], ..Default::default() }, ) .await?;
let client = Client::with_protocol(result.identity.clone(), HubProtocol::Wss)?; let reply = client.ask("Tell me a short clean joke.", RequestOptions::default()).await?; println!("{}", reply.text); client.close().await?;
Ok(())}Expected result:
Why did the hub keep good logs? It wanted every punchline to be traceable.Control plane
Discover hubs and create client identities.
Identity
Load saved credentials and choose a protocol.
Runtime client
Connect, ask, emit events, and send structured input.
Events and context
Listen for hub events and attach session metadata.
Use this before hub discovery or identity provisioning.
from thalovant import ThalovantControlPlane
api = ThalovantControlPlane()import { ThalovantControlPlane } from "@thalovant/sdk";
const api = new ThalovantControlPlane();import thalovant "github.com/thalovant/thalovant-go-sdk"
api := thalovant.NewDefaultControlPlane("")use thalovant::ControlPlane;
let mut api = ControlPlane::default();Sign in before private control-plane actions such as creating a client identity.
if err != nil { log.Fatal(err)}Common failure:
Missing Thalovant API access tokenFix it by signing in before the private call, or by passing a valid API token to the control-plane client where your SDK supports that.
Use public hub discovery before you ask a user to choose a hub.
page = api.list_public_hubs(limit=12)
for hub in page["data"]: print(hub["id"], hub["slug"], hub["title"])const page = await api.listPublicHubs({ limit: 12 });
for (const hub of page.data) { console.log(hub.id, hub.slug, hub.title);}page, err := api.ListPublicHubs(ctx, 12, "")if err != nil { log.Fatal(err)}
for _, hub := range page.Data { fmt.Println(hub.ID, hub.Slug, hub.Title)}let page = api.list_public_hubs(Some(12), None).await?;
for hub in page.data { println!("{} {} {}", hub.id, hub.slug, hub.title);}Use this when you have a hub reference from a URL, card, or saved choice.
hub = api.get_public_hub("joke-garden")print(hub["title"], hub["public_ref"])const hub = await api.getPublicHub("joke-garden");console.log(hub.title, hub.publicRef);hub, err := api.GetPublicHub(ctx, "joke-garden")if err != nil { log.Fatal(err)}fmt.Println(hub.Title, hub.PublicRef)let hub = api.get_public_hub("joke-garden").await?;println!("{} {}", hub.title, hub.public_ref);Use this for hubs the signed-in workspace can see.
page = api.list_hubs(limit=25)
for hub in page["data"]: print(hub["id"], hub["title"])const page = await api.listHubs({ limit: 25 });
for (const hub of page.data) { console.log(hub.id, hub.title);}page, err := api.ListHubs(ctx, 25, "", "")if err != nil { log.Fatal(err)}
for _, hub := range page.Data { fmt.Println(hub.ID, hub.Title)}let page = api.list_hubs(Some(25), None, None).await?;
for hub in page.data { println!("{} {}", hub.id, hub.title);}Use this after you already know the hub ID.
hub = api.get_hub("hub-id")print(hub["title"])const hub = await api.getHub("hub-id");console.log(hub.title);hub, err := api.GetHub(ctx, "hub-id")if err != nil { log.Fatal(err)}fmt.Println(hub.Title)let hub = api.get_hub("hub-id").await?;println!("{}", hub.title);Create an identity when a browser, service, voice client, device, or agent needs to connect to one hub.
result = api.create_client_identity( "hub-id", name="checkout-kiosk", preferred_protocols=("wss", "https"),)
identity = result.identityconst result = await api.createClientIdentity("hub-id", { name: "checkout-kiosk", preferredProtocols: ["wss", "https"],});
const identity = result.identity;result, err := api.CreateClientIdentityForHubID(ctx, "hub-id", thalovant.BootstrapIdentityOptions{ Name: "checkout-kiosk", PreferredProtocols: []thalovant.HubProtocol{thalovant.ProtocolWSS, thalovant.ProtocolHTTPS},})if err != nil { log.Fatal(err)}
identity := result.Identitylet result = api .create_client_identity_for_hub_id( "hub-id", BootstrapIdentityOptions { name: "checkout-kiosk".into(), preferred_protocols: vec![HubProtocol::Wss, HubProtocol::Https], ..Default::default() }, ) .await?;
let identity = result.identity.clone();Use config or a downloaded identity file when the client already exists.
from thalovant import ThalovantClient, ThalovantIdentity
identity = ThalovantIdentity.from_config(profile="prod")same_identity = ThalovantIdentity.from_file("_identity.json")
client_from_config = ThalovantClient.from_config(profile="prod", protocol="wss")client_from_file = ThalovantClient.from_identity_file("_identity.json")client_from_env = ThalovantClient.from_env()import { ThalovantClient, ThalovantIdentity } from "@thalovant/sdk";
const identity = await ThalovantIdentity.fromConfig({ profile: "prod" });const sameIdentity = await ThalovantIdentity.fromFile("_identity.json");
const clientFromConfig = await ThalovantClient.fromConfig({ profile: "prod", protocol: "wss" });const clientFromFile = await ThalovantClient.fromIdentityFile("_identity.json");const clientFromEnv = ThalovantClient.fromEnv();identity, err := thalovant.IdentityFromConfig("", "prod")if err != nil { log.Fatal(err)}
sameIdentity, err := thalovant.IdentityFromFile("_identity.json")if err != nil { log.Fatal(err)}
clientFromConfig, err := thalovant.NewClientFromConfig("", "prod")clientFromFile, err := thalovant.NewClientFromFile("_identity.json")clientFromEnv, err := thalovant.NewClientFromEnv()use thalovant::{Client, Identity};
let identity = Identity::from_config(Some("prod"))?;let same_identity = Identity::from_file("_identity.json")?;
let client_from_config = Client::from_config(Some("prod"))?;let client_from_file = Client::from_file("_identity.json")?;let client_from_env = Client::from_env()?;Use protocol helpers before forcing WSS, HTTPS, or MQTT.
print(identity.enabled_protocols())print(identity.endpoint_for("wss"))print(identity.endpoint_for("https"))print(identity.endpoint_for("mqtt"))print(identity.supports_protocol("wss"))
if identity.mqtt: print(identity.mqtt["host"], identity.mqtt["port"])console.log(identity.enabledProtocols());console.log(identity.endpointFor("wss"));console.log(identity.endpointFor("https"));console.log(identity.endpointFor("mqtt"));console.log(identity.supportsProtocol("wss"));
if (identity.mqtt) { console.log(identity.mqtt.host, identity.mqtt.port);}fmt.Println(identity.EnabledProtocols())fmt.Println(identity.EndpointFor(thalovant.ProtocolWSS))fmt.Println(identity.EndpointFor(thalovant.ProtocolHTTPS))fmt.Println(identity.EndpointFor(thalovant.ProtocolMQTT))fmt.Println(identity.SupportsProtocol(thalovant.ProtocolWSS))
if identity.MQTT != nil { fmt.Println(identity.MQTT.Host, identity.MQTT.Port)}println!("{:?}", identity.enabled_protocols());println!("{:?}", identity.endpoint_for(HubProtocol::Wss));println!("{:?}", identity.endpoint_for(HubProtocol::Https));println!("{:?}", identity.endpoint_for(HubProtocol::Mqtt));println!("{}", identity.supports_protocol(HubProtocol::Wss));
if let Some(mqtt) = &identity.mqtt { println!("{} {}", mqtt.host, mqtt.port);}Use a runtime client after you have identity material.
from thalovant import ThalovantClient
client = ThalovantClient(identity, protocol="wss")client.connect()print(client.healthcheck())client.close()import { ThalovantClient } from "@thalovant/sdk";
const client = new ThalovantClient(identity, { protocol: "wss" });await client.connect();console.log(await client.healthcheck());await client.close();client, err := thalovant.NewClientWithOptions(identity, thalovant.ClientOptions{ Protocol: thalovant.ProtocolWSS,})if err != nil { log.Fatal(err)}
if err := client.Connect(ctx); err != nil { log.Fatal(err)}fmt.Println(client.Healthcheck())client.Close(ctx)use thalovant::{Client, HubProtocol};
let client = Client::with_protocol(identity.clone(), HubProtocol::Wss)?;client.connect().await?;println!("{:?}", client.healthcheck().await);client.close().await?;Most code should use a context manager, try/finally, defer, or ? cleanup so connections close reliably.
Use ask for one request-response interaction.
reply = client.ask("What can this hub do?")
print(reply.text)for item in reply.display_items(): print(item)const reply = await client.ask("What can this hub do?");
console.log(reply.text);for (const item of reply.displayItems()) { console.log(item);}reply, err := client.Ask(ctx, "What can this hub do?", thalovant.RequestOptions{})if err != nil { log.Fatal(err)}
fmt.Println(reply.Text)for _, item := range reply.DisplayItems(600) { fmt.Println(item)}let reply = client .ask("What can this hub do?", RequestOptions::default()) .await?;
println!("{}", reply.text);for item in reply.display_items(Some(600)) { println!("{item:?}");}Expected output shape:
{ "text": "This hub can answer quick joke and trivia requests.", "display_items": []}Use raw events when you already have an event name and a structured payload.
client.emit( "thalovant.client.status", {"state": "ready"}, {"source": "checkout-kiosk"},)await client.emit( "thalovant.client.status", { state: "ready" }, { source: "checkout-kiosk" },);err := client.Emit(ctx, "thalovant.client.status", map[string]any{ "state": "ready",}, map[string]any{ "source": "checkout-kiosk",})if err != nil { log.Fatal(err)}use serde_json::json;
let mut data = serde_json::Map::new();data.insert("state".into(), json!("ready"));
let mut context = serde_json::Map::new();context.insert("source".into(), json!("checkout-kiosk"));
client .emit("thalovant.client.status", data, context) .await?;Use an utterance when the input should behave like speech or chat text from a client.
client.send_utterance( "What is the status?", lang="en-us", session_id="status-session",)await client.sendUtterance("What is the status?", { lang: "en-us", sessionId: "status-session",});err := client.SendUtterance(ctx, "What is the status?", thalovant.RequestOptions{ Lang: "en-us", SessionID: "status-session",})if err != nil { log.Fatal(err)}client .send_utterance( "What is the status?", RequestOptions { lang: Some("en-us".into()), session_id: Some("status-session".into()), ..Default::default() }, ) .await?;Use actions for buttons, menu picks, confirmations, and tool commands.
client.send_action( "/approve invoice-42", title="Approve invoice", session_id="approval-session",)await client.sendAction( "/approve invoice-42", { title: "Approve invoice", sessionId: "approval-session" },);err := client.SendAction(ctx, "/approve invoice-42", thalovant.ActionOptions{ Title: "Approve invoice", SessionID: "approval-session",})if err != nil { log.Fatal(err)}client .send_action( "/approve invoice-42", ActionOptions { title: Some("Approve invoice".into()), session_id: Some("approval-session".into()), ..Default::default() }, ) .await?;Use code input for QR values, barcode scans, serial numbers, short codes, or typed exact values.
client.send_code( "INV-2026-001", kind="invoice", session_id="scan-session",)await client.sendCode("INV-2026-001", { kind: "invoice", sessionId: "scan-session",});err := client.SendCode(ctx, "INV-2026-001", thalovant.CodeOptions{ Kind: "invoice", SessionID: "scan-session",})if err != nil { log.Fatal(err)}client .send_code( "INV-2026-001", CodeOptions { kind: Some("invoice".into()), session_id: Some("scan-session".into()), ..Default::default() }, ) .await?;Use a conversation when related turns should share the same session.
with client.conversation(lang="en-us") as convo: print(convo.ask("Remember that my favorite color is blue.").text) print(convo.ask("What color did I mention?").text)const convo = client.conversation({ lang: "en-us" });
const first = await convo.ask("Remember that my favorite color is blue.");const second = await convo.ask("What color did I mention?");console.log(first.text, second.text);convo := client.Conversation(thalovant.ConversationOptions{Lang: "en-us"})
first, err := convo.Ask(ctx, "Remember that my favorite color is blue.", thalovant.RequestOptions{})if err != nil { log.Fatal(err)}second, err := convo.Ask(ctx, "What color did I mention?", thalovant.RequestOptions{})if err != nil { log.Fatal(err)}fmt.Println(first.Text, second.Text)let convo = client.conversation(ConversationOptions { lang: Some("en-us".into()), ..Default::default()});
let first = convo .ask("Remember that my favorite color is blue.", RequestOptions::default()) .await?;let second = convo .ask("What color did I mention?", RequestOptions::default()) .await?;println!("{} {}", first.text, second.text);Use this when one event proves the request completed.
from thalovant import EVENT_UTTERANCE_HANDLED
event = client.wait_for_event(EVENT_UTTERANCE_HANDLED, timeout=12)print(event.text)import { EVENT_UTTERANCE_HANDLED } from "@thalovant/sdk";
const event = await client.waitForEvent(EVENT_UTTERANCE_HANDLED, { timeoutMs: 12_000,});console.log(event.text);events := client.Transport.Events()
select {case event := <-events: fmt.Println(event.Name, event.Text())case <-time.After(12 * time.Second): log.Fatal("timed out waiting for event")}use std::time::Duration;use tokio::time::timeout;
let mut events = client.transport.subscribe();
if let Ok(Ok(event)) = timeout(Duration::from_secs(12), events.recv()).await { println!("{} {}", event.name, event.text());}Use event streams for long-running clients, live status, speech output, and UI updates.
from thalovant import EVENT_SPEAK
for event in client.listen(EVENT_SPEAK, timeout=30, max_events=3): print(event.text)import { EVENT_SPEAK } from "@thalovant/sdk";
const subscription = client.on(EVENT_SPEAK, event => { console.log(event.text);});
// Later, when the listener is no longer needed:subscription.close();events := client.Transport.Events()
for event := range events { fmt.Println(event.Name, event.Text())}let mut events = client.transport.subscribe();
while let Ok(event) = events.recv().await { println!("{} {}", event.name, event.text());}Context carries stable user, source, locale, and trace metadata without changing the utterance.
from thalovant import build_client_context
context = build_client_context( user_id="user-42", user_name="Ada", source="checkout-kiosk", platform="kiosk", locale="en-US", metadata={"trace_id": "req-2026-06-10-001"},)
reply = client.ask("Show the next instruction.", context=context)print(reply.text)import { buildClientContext } from "@thalovant/sdk";
const context = buildClientContext({}, { userId: "user-42", userName: "Ada", source: "checkout-kiosk", platform: "kiosk", locale: "en-US", channel: "chat", metadata: { traceId: "req-2026-06-10-001" },});
const reply = await client.ask("Show the next instruction.", { context });console.log(reply.text);requestContext := thalovant.BuildClientContext(nil, thalovant.ClientContextOptions{ UserID: "user-42", UserName: "Ada", Source: "checkout-kiosk", Platform: "kiosk", Locale: "en-US", Channel: "chat", Metadata: map[string]any{"trace_id": "req-2026-06-10-001"},})
reply, err := client.Ask(ctx, "Show the next instruction.", thalovant.RequestOptions{ Context: requestContext,})use serde_json::json;
let mut metadata = serde_json::Map::new();metadata.insert("trace_id".into(), json!("req-2026-06-10-001"));
let context = build_client_context(None, ClientContextOptions { user_id: Some("user-42".into()), user_name: Some("Ada".into()), source: Some("checkout-kiosk".into()), platform: Some("kiosk".into()), locale: Some("en-US".into()), channel: Some("chat".into()), metadata: Some(metadata), ..Default::default()});
let reply = client .ask( "Show the next instruction.", RequestOptions { context: Some(context), ..Default::default() }, ) .await?;Use diagnostics when a client cannot connect, a protocol is missing, or an event never arrives.
print(client.doctor())console.log(await client.healthcheck());fmt.Println(client.Healthcheck())println!("{:?}", client.healthcheck().await);Use the SDK protocol enum or string that matches your language.
| Protocol | Python | Node.js | Go | Rust |
|---|---|---|---|---|
| WSS | "wss" | "wss" | thalovant.ProtocolWSS | HubProtocol::Wss |
| HTTPS | "https" | "https" | thalovant.ProtocolHTTPS | HubProtocol::Https |
| MQTT | "mqtt" | "mqtt" | thalovant.ProtocolMQTT | HubProtocol::Mqtt |
When no protocol is forced, SDKs prefer WSS, then HTTPS, then MQTT when the identity includes broker credentials.
list_public_hubs, get_public_hub, list_hubs, or get_hub.create_client_identity for new clients, or load an existing identity from config, file, or environment.ask first because it gives a clear reply.listen, wait_for_event, and build_client_context once the basic request works.Use this table only when you already know the task and need the language-specific name.
| Task | Python | Node.js | Go | Rust |
|---|---|---|---|---|
| Create API client | ThalovantControlPlane() | new ThalovantControlPlane() | NewDefaultControlPlane(token) | ControlPlane::default() |
| Sign in | login(email, password) | login(email, password) | Login(ctx, email, password, scope) | login(email, password, scope) |
| List public hubs | list_public_hubs(...) | listPublicHubs(...) | ListPublicHubs(ctx, limit, cursor) | list_public_hubs(limit, cursor) |
| Get public hub | get_public_hub(ref) | getPublicHub(ref) | GetPublicHub(ctx, ref) | get_public_hub(ref) |
| List visible hubs | list_hubs(...) | listHubs(...) | ListHubs(ctx, limit, cursor, ownerID) | list_hubs(limit, cursor, owner_id) |
| Get hub | get_hub(hub_id) | getHub(hubId) | GetHub(ctx, hubID) | get_hub(hub_id) |
| Create identity | create_client_identity(...) | createClientIdentity(...) | CreateClientIdentityForHubID(...) | create_client_identity_for_hub_id(...) |
| Task | Python | Node.js | Go | Rust |
|---|---|---|---|---|
| Load identity from config | ThalovantIdentity.from_config(...) | ThalovantIdentity.fromConfig(...) | IdentityFromConfig(path, profile) | Identity::from_config(profile) |
| Load identity file | ThalovantIdentity.from_file(path) | ThalovantIdentity.fromFile(path) | IdentityFromFile(path) | Identity::from_file(path) |
| Load client from file | ThalovantClient.from_identity_file(path) | ThalovantClient.fromIdentityFile(path) | NewClientFromFile(path) | Client::from_file(path) |
| Load client from config | ThalovantClient.from_config(...) | ThalovantClient.fromConfig(...) | NewClientFromConfig(path, profile) | Client::from_config(profile) |
| Load client from env | ThalovantClient.from_env() | ThalovantClient.fromEnv() | NewClientFromEnv() | Client::from_env() |
| Enabled protocols | enabled_protocols() | enabledProtocols() | EnabledProtocols() | enabled_protocols() |
| Endpoint for protocol | endpoint_for(protocol) | endpointFor(protocol) | EndpointFor(protocol) | endpoint_for(protocol) |
| Check protocol | supports_protocol(protocol) | supportsProtocol(protocol) | SupportsProtocol(protocol) | supports_protocol(protocol) |
| MQTT credentials | identity.mqtt | identity.mqtt | Identity.MQTT | identity.mqtt |
| Task | Python | Node.js | Go | Rust |
|---|---|---|---|---|
| Create protocol client | ThalovantClient(identity, protocol="wss") | new ThalovantClient(identity, { protocol }) | NewClientWithOptions(identity, ClientOptions{Protocol: ...}) | Client::with_protocol(identity, protocol) |
| Connect | connect() | connect() | Connect(ctx) | connect() |
| Close | close() | close() | Close(ctx) | close() |
| Health | healthcheck() | healthcheck() | Healthcheck() | healthcheck() |
| Ask | ask(text, ...) | ask(text, options) | Ask(ctx, text, options) | ask(text, options) |
| Emit raw event | emit(event, data, context) | emit(event, data, context) | Emit(ctx, event, data, context) | emit(event, data, context) |
| Send utterance | send_utterance(text, ...) | sendUtterance(text, options) | SendUtterance(ctx, text, options) | send_utterance(text, options) |
| Send action | send_action(payload, ...) | sendAction(payload, options) | SendAction(ctx, payload, options) | send_action(payload, options) |
| Send code | send_code(value, ...) | sendCode(value, options) | SendCode(ctx, value, options) | send_code(value, options) |
| Conversation | conversation(...) | conversation(options) | Conversation(options) | conversation(options) |
| Task | Python | Node.js | Go | Rust |
|---|---|---|---|---|
| Wait for event | wait_for_event(name, ...) | waitForEvent(name, options) | client.Transport.Events() with timeout | client.transport.subscribe() with timeout |
| Stream events | listen(name, ...) | on(name, handler, options) | client.Transport.Events() | client.transport.subscribe() |
| Diagnostics | doctor() | healthcheck() | Healthcheck() | healthcheck() |
| Build context | build_client_context(...) | buildClientContext(...) | BuildClientContext(...) | build_client_context(...) |
| Response text | reply.text | reply.text | reply.Text | reply.text |
| Display items | reply.display_items(...) | reply.displayItems(...) | reply.DisplayItems(...) | reply.display_items(...) |
| Event text | event.text | event.text | event.Text() | event.text() |