SDKs

Official client libraries for the Hot API, released in lockstep versions:

LanguagePackageSourceAPI Reference
JavaScript / TypeScript@hot-dev/sdk (npm)hot-dev/hot-jsPackage README
Pythonhot-dev (PyPI)hot-dev/hot-pythonREADME
Gogithub.com/hot-dev/hot-gohot-dev/hot-gopkg.go.dev
Rusthot-dev (crates.io)hot-dev/hot-rustdocs.rs
Javadev.hot:hot-sdk (Maven Central)hot-dev/hot-javajavadoc.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.

LanguageWait method
JavaScript / TypeScriptawait hot.runs.wait(runId)
Pythonhot.runs.wait(run_id) or await async_hot.runs.wait(run_id)
Goclient.Runs.Wait(ctx, runID, nil)
Rustclient.runs().wait(run_id, RunWaitOptions::default()).await
Javaclient.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 as event_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. subscribeWithEvent resubscribes across the API's 5-minute SSE timeout, dedupes replayed run:start and terminal events by run_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 plain subscribe when your app expects multiple independent runs on one stream. A plain stream subscription also carries durable task:update snapshots for tasks associated with that stream.
  • Durable run waiting. runs.wait (or the language-equivalent method) reads run:update snapshots and reconnects without missing a run that became terminal before subscription setup.
  • Durable task waiting. tasks.wait (or the language-equivalent method) reads task:update snapshots, 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.