> ## Documentation Index
> Fetch the complete documentation index at: https://docs.karta.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless & React

> Skip the prebuilt UI: build your own chat on the headless KartaAgentClient, or use the @karta/react bindings.

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.

```bash theme={null}
npm install @karta/widget
```

```ts theme={null}
import { KartaAgentClient } from "@karta/widget";

const client = new KartaAgentClient({
  baseUrl: "https://agent.karta.sh",
  agentRef: "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, "approve_once"); // 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;
  }
}
```

<Warning>
  Assistant `message` text is **cumulative** - each event carries the full
  message so far. **Replace** the rendered bubble on every event; never
  concatenate.
</Warning>

### 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<AgentEvent[]>` - adopt an existing session, rebuild
  its cursor from event history, and return its prior events (normalized
  `AgentEvent`s) so a UI can rebuild the transcript.
* `listSessions(): Promise<SessionSummary[]>` - the end-user's past
  conversations, newest first, for a session sidebar.
* `openSession(sessionId): Promise<TranscriptMessage[]>` - reopen a past
  conversation: load its transcript for display and adopt the session so the next
  `sendMessage` continues it.
* `newSession(): void` - drop the active session so the next `sendMessage` opens
  a fresh one.
* `confirmTool(requestId, decision): Promise<void>` - respond to an
  `input_required` pause whose `kind` is `"tool_permission"`. `decision` is one of
  `"approve_once"` (allow this one call), `"approve_session"` (allow this and the
  rest of the session), or `"deny"`.
* `answerQuestion(requestId, answers): Promise<void>` - respond to an
  `input_required` pause whose `kind` is `"user_question"` (the agent asked the
  user something). Render the pause's `questions` as choices, then pass `answers`
  as an object keyed by question text; for a `multiSelect` question join the
  chosen labels with `", "`. Answering never grants a tool for the session.
* `uploadFile(name, contentBase64): Promise<string>` - upload a file to the
  session; returns its workspace-relative path. Opens a session first if none
  exists.
* `interrupt(): Promise<void>` - ask the server to stop the current turn.
* `identify(user): void` - set [soft / verified identity](/sdks/widget/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`:

```ts theme={null}
const client = new KartaAgentClient({
  baseUrl: "https://agent.karta.sh",
  agentRef: "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.

```bash theme={null}
npm install @karta/react react react-dom
```

### `<KartaWidget/>` - the prebuilt widget as a component

```tsx theme={null}
import { KartaWidget } from "@karta/react";

export function App() {
  return (
    <KartaWidget
      agent="coffeeco/support-bot"
      embedKey="pk_live_..."
      user={{ id: "user_123", identityToken: serverMintedToken }}
      theme={{ accent: "#d4ff4f", agentName: "Beans" }}
    />
  );
}
```

It mounts into `document.body` as an iframe (the agent-chat app in widget mode),
so `<KartaWidget/>` itself puts nothing in your React tree. It is **SSR-safe**
(mounts in a client-only effect) and
re-mounts only when an identity-defining prop changes (`agent`, `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:

```tsx theme={null}
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} agent="coffeeco/support-bot" embedKey="pk_live_..." />
    </>
  );
}
```

The handle exposes `open()`, `close()`, and `sendMessage(text)`.

<Note>
  `<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.
</Note>

### `useKartaAgent()` - build a custom UI

```tsx theme={null}
import { useKartaAgent } from "@karta/react";

function Chat() {
  const { messages, send, status, error, reset } = useKartaAgent({
    agent: "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
  agent message, `text` is **cumulative** and is REPLACED on each event (the
  hook handles this - never concatenate).
* `send(text)` - appends a user message and a streaming agent 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

<CardGroup cols={2}>
  <Card title="Identity" icon="user-check" href="/sdks/widget/identity">
    Soft vs verified, and the HMAC scheme the `user`/`identify` fields feed.
  </Card>

  <Card title="Security & privacy" icon="shield-halved" href="/security/chat-widget">
    The credential boundaries and the data path.
  </Card>
</CardGroup>
