Skip to main content
The prebuilt widget covers the common case. When you need your own UI - a full-page chat, a custom panel, native React components - use the headless client or the React bindings directly. Both drive the same streaming core; they do not reimplement it.

Headless: @karta/widget

KartaAgentClient is a transport- and UI-agnostic client. Zero runtime dependencies, ESM-only, strict TypeScript. It talks to the Managed Agents consumer API by default (the richest token-authed surface: decoupled POST-event / GET-stream gives reload-resume and tool confirmation), with the OpenAI Responses API as a portable fallback.
npm install @karta/widget
import { KartaAgentClient } from "@karta/widget";

const client = new KartaAgentClient({
  baseUrl: "https://agent.karta.sh",
  projectRef: "coffeeco/support-bot",
  embedKey: "pk_live_xxx",
});

for await (const event of client.sendMessage("How do I reset my password?")) {
  switch (event.type) {
    case "message":
      // `text` is the FULL message so far - REPLACE the bubble, do not append.
      render(event.text);
      break;
    case "tool_use":
      console.log("tool:", event.tool, event.input);
      break;
    case "input_required":
      await client.confirmTool(event.requestId, true); // approve a paused tool
      for await (const e2 of client.stream()) render(e2.type === "message" ? e2.text : "");
      break;
    case "done":
      console.log("usage:", event.usage);
      break;
    case "error":
      console.error(event.message);
      break;
  }
}
Assistant message text is cumulative - each event carries the full message so far. Replace the rendered bubble on every event; never concatenate.

Methods

  • createSession(): Promise<{ sessionId }> - open a Managed Agents session.
  • sendMessage(text): AsyncGenerator<AgentEvent> - ensure a session, post the message, stream the turn until done/error.
  • stream(fromSeq?): AsyncGenerator<AgentEvent> - low-level resume of an in-flight turn from a cursor.
  • resume(sessionId): Promise<void> - adopt an existing session and rebuild its cursor from event history.
  • confirmTool(requestId, allow): Promise<void> - respond to an input_required pause.
  • interrupt(): Promise<void> - ask the server to stop the current turn.
  • identify(user): void - set soft / verified identity, applied on the next createSession.
  • shutdown(): void - abort all in-flight work and close the client.

Authentication

Pass exactly one auth source: a publishable embedKey (the client exchanges it at /v1/embed/session-tokens), a static token, a tokenEndpoint URL the client GETs for { token }, or a refreshable tokenFn:
const client = new KartaAgentClient({
  baseUrl: "https://agent.karta.sh",
  projectRef: "coffeeco/support-bot",
  tokenFn: async () => {
    const res = await fetch("/api/karta-token");
    return (await res.json()).token; // re-called with { force: true } after a 401
  },
});

React: @karta/react

A thin layer over @karta/widget with two entry points. react and react-dom (>=18) are peer dependencies.
npm install @karta/react react react-dom

<KartaWidget/> - the prebuilt widget as a component

import { KartaWidget } from "@karta/react";

export function App() {
  return (
    <KartaWidget
      project="coffeeco/support-bot"
      embedKey="pk_live_..."
      user={{ id: "user_123", identityToken: serverMintedToken }}
      theme={{ accent: "#d4ff4f", agentName: "Beans" }}
    />
  );
}
It renders into document.body via Shadow DOM, so <KartaWidget/> itself puts nothing in your tree. It is SSR-safe (mounts in a client-only effect) and re-mounts only when an identity-defining prop changes (project, embedKey, baseUrl, token, tokenEndpoint, transport, user.id, user.identityToken) - changing theme or a callback updates in place without dropping the conversation. Wire host-page events via callback props (onReady, onOpen, onClose, onSessionStarted, onMessageSent, onMessageReceived, onUnread, onEscalate, onError), and drive it from a ref:
import { useRef } from "react";
import { KartaWidget, type KartaWidgetHandle } from "@karta/react";

function Support() {
  const widget = useRef<KartaWidgetHandle>(null);
  return (
    <>
      <button onClick={() => widget.current?.open()}>Need help?</button>
      <KartaWidget ref={widget} project="coffeeco/support-bot" embedKey="pk_live_..." />
    </>
  );
}
The handle exposes open(), close(), and sendMessage(text).
<KartaWidget/> does not accept tokenFn or model. For a refreshable token function or a custom model, build a custom UI with useKartaAgent() below, which exposes the full client option surface.

useKartaAgent() - build a custom UI

import { useKartaAgent } from "@karta/react";

function Chat() {
  const { messages, send, status, error, reset } = useKartaAgent({
    project: "coffeeco/support-bot",
    embedKey: "pk_live_...",
    user: { id: "user_123", identityToken: serverMintedToken },
  });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id} className={m.role}>{m.text}</div>
      ))}
      {error && <p role="alert">{error.message}</p>}
      <form onSubmit={(e) => {
        e.preventDefault();
        const input = e.currentTarget.elements.namedItem("msg") as HTMLInputElement;
        void send(input.value);
        input.value = "";
      }}>
        <input name="msg" disabled={status === "streaming"} />
        <button disabled={status === "streaming"}>Send</button>
      </form>
      <button onClick={reset}>New chat</button>
    </div>
  );
}
useKartaAgent() returns { messages, send, status, error, sessionId, reset }:
  • messages: ChatMessage[] - { id, role, text, tools?, streaming? }. For an assistant message, text is cumulative and is REPLACED on each event (the hook handles this - never concatenate).
  • send(text) - appends a user message + a streaming assistant message, then consumes the turn.
  • status - 'idle' | 'streaming' | 'error'.
  • error - the Error from the last failed turn (carries code when present).
  • sessionId - the backing session id once a turn has started.
  • reset() - clears the transcript and aborts any in-flight stream.
The client is recreated only when an identity-defining option changes; the in-flight stream is aborted on unmount and on reset().

Next

Identity

Soft vs verified, and the HMAC scheme the user/identify fields feed.

Security & privacy

The credential boundaries and the data path.