SDKs
Official client libraries for the Hot API, released in lockstep versions:
| Language | Package | Source | API Reference |
|---|---|---|---|
| JavaScript / TypeScript | @hot-dev/sdk (npm) | hot-dev/hot-js | Package README |
| Python | hot-dev (PyPI) | hot-dev/hot-python | README |
| Go | github.com/hot-dev/hot-go | hot-dev/hot-go | pkg.go.dev |
| Rust | hot-dev (crates.io) | hot-dev/hot-rust | docs.rs |
| Java | dev.hot:hot-sdk (Maven Central) | hot-dev/hot-java | javadoc.io |
Every SDK covers the full API v1 surface: the thirteen resources (Endpoints), SSE run-stream subscriptions with automatic reconnection across the API's 5-minute stream timeout, durable run and task waiters, structured API errors, and escape hatches for endpoints that do not yet have a helper.
Authenticated clients should run server-side. Browser apps and untrusted
clients should call your own backend route instead of exposing a Hot API key
(the JavaScript SDK ships a @hot-dev/sdk/proxy helper for this).
Install
JavaScript
npm install @hot-dev/sdk
Requires Node 20+. ESM-only.
Python
pip install hot-dev
Python 3.10+. Import as hot.
Go
go get github.com/hot-dev/hot-go
Go 1.23+. Zero dependencies.
Rust
cargo add hot-dev tokio --features tokio/full
cargo add futures-util serde_json
Async on tokio; TLS via rustls. Import as hot_dev.
Java
// Gradle
implementation("dev.hot:hot-sdk:1.1.3")
<!-- Maven -->
<dependency>
<groupId>dev.hot</groupId>
<artifactId>hot-sdk</artifactId>
<version>1.1.3</version>
</dependency>
Java 17+. Jackson is the only runtime dependency.
Quick Start
Publish an event and stream its run to completion. base_url defaults to
https://api.hot.dev; for local development with hot dev, point it at
http://localhost:4681.
JavaScript
import { HotClient } from "@hot-dev/sdk";
const hot = new HotClient({ token: process.env.HOT_API_KEY });
for await (const event of hot.streams.subscribeWithEvent({
event_type: "team-agent:ask",
event_data: { question: "what is blocking launch?" },
})) {
if (event.type === "stream:data") console.log(event.data_type, event.payload);
if (event.type === "run:stop") {
console.log(event.run?.result);
break;
}
}
Python
import os
from hot import HotClient
hot = HotClient(token=os.environ["HOT_API_KEY"])
for event in hot.streams.subscribe_with_event(
{"event_type": "team-agent:ask", "event_data": {"question": "what is blocking launch?"}}
):
if event["type"] == "stream:data":
print(event["data_type"], event.get("payload"))
if event["type"] == "run:stop":
print(event.get("run", {}).get("result"))
break
Go
client, err := hot.NewClient(hot.Config{Token: os.Getenv("HOT_API_KEY")})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
for event, err := range client.Streams.SubscribeWithEvent(ctx, map[string]any{
"event_type": "team-agent:ask",
"event_data": map[string]any{"question": "what is blocking launch?"},
}, nil) {
if err != nil {
log.Fatal(err)
}
if event.Type() == "stream:data" {
fmt.Println(event["data_type"], event["payload"])
}
if event.Type() == "run:stop" {
fmt.Println(event.Run()["result"])
break
}
}
Rust
use futures_util::StreamExt;
use hot_dev::{HotClient, StreamEventExt, SubscribeWithEventOptions};
use serde_json::json;
let client = HotClient::builder(std::env::var("HOT_API_KEY").unwrap()).build();
let mut events = client.streams().subscribe_with_event(
json!({
"event_type": "team-agent:ask",
"event_data": { "question": "what is blocking launch?" },
}),
SubscribeWithEventOptions::default(),
);
while let Some(event) = events.next().await {
let event = event?;
if event.event_type() == "stream:data" {
println!("{:?} {:?}", event.get("data_type"), event.get("payload"));
}
if event.event_type() == "run:stop" {
println!("{:?}", event.run().and_then(|run| run.get("result")));
break;
}
}
Java
HotClient client = HotClient.builder(System.getenv("HOT_API_KEY")).build();
try (StreamEvents events = client.streams().subscribeWithEvent(Map.of(
"event_type", "team-agent:ask",
"event_data", Map.of("question", "what is blocking launch?")))) {
while (events.hasNext()) {
Map<String, Object> event = events.next();
if (StreamEvents.typeOf(event).equals("stream:data")) {
System.out.println(event.get("data_type") + " " + event.get("payload"));
}
if (StreamEvents.typeOf(event).equals("run:stop")) {
System.out.println(StreamEvents.runOf(event).get("result"));
break;
}
}
}
Call a Hot Function
Every SDK wraps the publish-and-wait flow for hot:call events. The helper
publishes through the atomic subscribe-with-event endpoint, correlates the
terminal run to the published event, and reconnects without publishing twice:
JavaScript
const result = await hot.events.callHot("::myapp::math/add-nums", [2, 3]);
// result === 5
Python
result = hot.events.call_hot("::myapp::math/add-nums", [2, 3])
# result == 5
Go
result, err := client.Events.CallHot(ctx, "::myapp::math/add-nums", []any{2, 3}, nil)
// result == float64(5)
Rust
let result = client
.events()
.call_hot("::myapp::math/add-nums", vec![json!(2), json!(3)], CallOptions::default())
.await?;
// result == json!(5)
Java
Object result = client.events().callHot("::myapp::math/add-nums", List.of(2, 3));
// result equals 5
Wait for a Run
If another API response or stored record gives your client a run ID, wait on that run directly. The run subscription sends the latest persisted snapshot first, reconnects after transport interruptions, and closes after a terminal state. A failed or cancelled run raises a structured run error containing the terminal run record.
| Language | Wait method |
|---|---|
| JavaScript / TypeScript | await hot.runs.wait(runId) |
| Python | hot.runs.wait(run_id) or await async_hot.runs.wait(run_id) |
| Go | client.Runs.Wait(ctx, runID, nil) |
| Rust | client.runs().wait(run_id, RunWaitOptions::default()).await |
| Java | client.runs().waitFor(runId) |
Use the stream subscription when you need live stream:data or several
related runs. Use the run waiter when one run's durable terminal record is the
only result you need.
Wait for a Background Task
When a Hot run starts a task for a client, return the task id immediately and let the SDK wait for its durable result. The task subscription sends the latest persisted snapshot first, including a terminal snapshot when the task completed before the client subscribed. Waiters reconnect automatically and return the completed task record. Failed, cancelled, and timed-out tasks raise a structured task error containing that record.
JavaScript
const task = await hot.tasks.wait(taskId, { timeoutMs: 300_000 });
console.log(task.result);
Python
task = hot.tasks.wait(task_id, timeout=300)
print(task["result"])
# AsyncHotClient exposes the same resource:
task = await async_hot.tasks.wait(task_id, timeout=300)
Go
task, err := client.Tasks.Wait(ctx, taskID, &hot.WaitForTaskOptions{
Timeout: 5 * time.Minute,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(task["result"])
Rust
let task = client
.tasks()
.wait(task_id, TaskWaitOptions {
timeout: Duration::from_secs(300),
..TaskWaitOptions::default()
})
.await?;
println!("{:?}", task.get("result"));
Java
Map<String, Object> task = client.tasks().waitFor(
taskId,
new TaskWaitOptions().timeout(Duration.ofMinutes(5)));
System.out.println(task.get("result"));
Use ::hot::task/await inside Hot only when later Hot code in that same
execution depends on the task result. Keeping a run alive just so a client can
wait defeats the task's asynchronous lifecycle.
Coordinate several tasks on one stream
The stream that contains the originating run also emits durable task:update
snapshots for every task on that stream. This keeps the event-handler result
fully user-defined: it can return one task ID, two task IDs, or a nested domain
object. The client interprets that result, then uses the same stream to follow
all relevant tasks:
let streamId: string | undefined;
let taskIds: string[] = [];
for await (const event of hot.streams.subscribeWithEvent({
event_type: "report:requested",
event_data: { report_id: "report_123" },
})) {
if (event.type === "event:published") streamId = event.stream_id;
if (event.type === "run:stop") {
// `task_ids` is application-defined, not imposed by Hot.
const result = event.run?.result as { task_ids?: string[] } | undefined;
taskIds = result?.task_ids ?? [];
break;
}
}
const pending = new Set(taskIds);
for await (const event of hot.streams.subscribe(streamId!)) {
if (event.type !== "task:update" || !pending.has(event.task.task_id)) continue;
if (["failed", "cancelled", "timed_out"].includes(event.task.status)) {
throw new Error(`Task ${event.task.task_id} ${event.task.status}`);
}
if (event.task.status === "completed") pending.delete(event.task.task_id);
if (pending.size === 0) break;
}
Because the stream sends current persisted task snapshots when the client
connects, this remains safe if one of the tasks finishes between the handler's
run:stop and the second subscription. For a single task, tasks.wait(taskId)
wraps the same durable lifecycle with reconnection, timeout, and structured
failure handling.
Errors
Non-2xx responses surface as structured errors with status_code, code,
request_id, and retry_after:
JavaScript
import { HotApiError } from "@hot-dev/sdk";
try {
await hot.projects.get("missing-project");
} catch (error) {
if (error instanceof HotApiError) {
console.log(error.status, error.code, error.requestId, error.retryAfter);
}
}
Python
from hot import HotApiError
try:
hot.projects.get("missing-project")
except HotApiError as error:
print(error.status_code, error.code, error.request_id, error.retry_after)
Go
_, err := client.Projects.Get(ctx, "missing-project")
var apiErr *hot.APIError
if errors.As(err, &apiErr) {
fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.RequestID, apiErr.RetryAfter)
}
Rust
match client.projects().get("missing-project").await {
Err(hot_dev::Error::Api(error)) => {
println!("{} {:?} {:?} {:?}", error.status_code, error.code, error.request_id, error.retry_after);
}
other => drop(other),
}
Java
try {
client.projects().get("missing-project");
} catch (HotApiException error) {
System.out.println(error.statusCode() + " " + error.code() + " "
+ error.requestId() + " " + error.retryAfter());
}
Shared Behavior
All five SDKs follow the same conventions:
- Wire-format payloads. Request and response payloads use the API wire
format (
event_type,event_data,stream_id). SDK-only options use each language's idiom (baseUrl,base_url,BaseURL). No SDK ever transforms user-owned payloads such asevent_data. - Retries. JSON requests retry automatically (at most twice) when the API
responds 429 with a
retry_after. Streaming and raw requests never retry. - Streaming reconnection.
subscribeWithEventresubscribes across the API's 5-minute SSE timeout, dedupes replayedrun:startand terminal events byrun_id, and ends after the terminal run correlated to the event it published. Unrelated runs on the same stream do not end the iterator. Use the plainsubscribewhen your app expects multiple independent runs on one stream. A plain stream subscription also carries durabletask:updatesnapshots for tasks associated with that stream. - Durable run waiting.
runs.wait(or the language-equivalent method) readsrun:updatesnapshots and reconnects without missing a run that became terminal before subscription setup. - Durable task waiting.
tasks.wait(or the language-equivalent method) readstask:updatesnapshots, reconnects across transport interruptions, and cannot miss a task that became terminal before subscription setup. - Identification. Each SDK sends
User-Agent: hot-sdk-<lang>/<version>. - Escape hatches. A
request(...)method (plus a raw-response variant) covers endpoints that do not yet have a resource helper.
Building chat or agent frontends? The JavaScript SDK additionally ships
agent, webhook, and BFF proxy helpers as subpath exports
(@hot-dev/sdk/agent, /webhook, /proxy); the other SDKs intentionally
cover the core API.