# Build your own chat UI

> Use the public chat API to put your AI in a chat you design yourself, with the Vercel AI SDK or a few lines of plain JavaScript.

The [website widget](/docs/website/quickstart) runs on a public API, and so can your own chat. Use it when you want the conversation in your design system, inside an existing app, or in a UI that's entirely yours.

It takes two calls from your web page:

1. **Start a session** when the visitor sends their first message. `POST /sessions` returns a session token for that visitor's conversation.
2. **Send messages.** `POST /chat`, with the token in an `X-Uare-Session` header, streams your AI's reply back as it's written.

```text
https://app.uare.ai/api/v1/public/ai/your-handle
```

There's no API key. Your AI answers web pages on the [websites its owner allows](/docs/website/quickstart#1-allow-your-website), in public mode, the way it answers visitors on its public profile. The owner adds them in their Settings on uare.ai, `http://localhost:3000` or whichever local address you use included, to try it while you build.

The reply streams in the [AI SDK](https://ai-sdk.dev) UI message format, so `useChat` reads it directly. Anything that reads server-sent events can read it too.

## React with the AI SDK

Install the AI SDK:

```bash
npm install ai @ai-sdk/react
```

Then give `useChat` a transport whose headers start the session the first time the visitor sends a message, and reuse it after that:

```tsx title="Chat.tsx"
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useRef, useState } from "react";

const API = "https://app.uare.ai/api/v1/public/ai/your-handle";

type Session = { session_id: string; token: string };

async function startSession(): Promise<Session> {
  const res = await fetch(`${API}/sessions`, { method: "POST" });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error?.message ?? `The chat couldn't start (${res.status}).`);
  return body;
}

export function Chat() {
  const [input, setInput] = useState("");
  const session = useRef<Promise<Session> | null>(null);
  const { messages, sendMessage, status, error } = useChat({
    transport: new DefaultChatTransport({
      api: `${API}/chat`,
      // No session until the visitor's first message; every message after it reuses the session.
      headers: async () => {
        session.current ??= startSession().catch((err) => {
          session.current = null; // the next message tries again
          throw err;
        });
        return { "X-Uare-Session": (await session.current).token };
      },
    }),
  });
  const busy = status === "submitted" || status === "streaming";

  return (
    <div>
      {messages.map((message) => (
        <p key={message.id}>
          <strong>{message.role === "user" ? "You" : "AI"}: </strong>
          {message.parts.map((part, i) => (part.type === "text" ? <span key={i}>{part.text}</span> : null))}
        </p>
      ))}
      {error ? <p>{error.message}</p> : null}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          sendMessage({ text: input });
          setInput("");
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
        <button type="submit" disabled={busy || !input.trim()}>Send</button>
      </form>
    </div>
  );
}
```

Your AI's replies often use markdown. Render text parts with a markdown component, such as [Streamdown](https://streamdown.ai), for headings, lists and links.

The stream also carries your AI's suggested follow-ups as a `data-suggestions` part, and transient `data-quota` and `data-status` parts you can read with `useChat`'s `onData` callback. [Stream parts](/docs/api/reference#stream-parts) lists them all.

## Plain JavaScript

No library needed: a form, and a script that starts a session with the visitor's first message, reads the stream and handles each `data:` line.

```html title="index.html"
<form id="chat">
  <input id="message" placeholder="Ask me anything" autocomplete="off" />
  <button>Send</button>
</form>
<p id="reply"></p>

<script type="module" src="chat.js"></script>
```

```js title="chat.js"
const API = "https://app.uare.ai/api/v1/public/ai/your-handle";

// The session starts with the visitor's first message; later messages reuse it.
let session = null;

async function sessionToken() {
  session ??= fetch(`${API}/sessions`, { method: "POST" }).then(async (res) => {
    const body = await res.json();
    if (!res.ok) throw new Error(body.error?.message ?? `The chat couldn't start (${res.status}).`);
    return body.token;
  });
  try {
    return await session;
  } catch (error) {
    session = null; // the next message tries again
    throw error;
  }
}

async function ask(text, onText) {
  const res = await fetch(`${API}/chat`, {
    method: "POST",
    headers: { "content-type": "application/json", "x-uare-session": await sessionToken() },
    body: JSON.stringify({ text }),
  });
  if (!res.ok) {
    const body = await res.json().catch(() => null);
    throw new Error(body?.error?.message ?? `The chat failed (${res.status}).`);
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) return;
    buffer += decoder.decode(value, { stream: true });
    let end;
    while ((end = buffer.indexOf("\n\n")) !== -1) {
      const line = buffer.slice(0, end);
      buffer = buffer.slice(end + 2);
      if (!line.startsWith("data: ")) continue;
      const data = line.slice(6);
      if (data === "[DONE]") return;
      const part = JSON.parse(data);
      if (part.type === "text-delta") onText(part.delta);
      if (part.type === "error") throw new Error(part.errorText);
    }
  }
}

document.querySelector("#chat").addEventListener("submit", async (event) => {
  event.preventDefault();
  const input = document.querySelector("#message");
  const reply = document.querySelector("#reply");
  const text = input.value.trim();
  if (!text) return;
  input.value = "";
  reply.textContent = "";
  try {
    await ask(text, (delta) => reply.append(delta));
  } catch (error) {
    reply.textContent = error.message;
  }
});
```

This page shows the reply as plain text: your AI's replies often use markdown, and its marks (`**bold**`, `- ` lists, links) appear as typed. To format them, render the text with a markdown library and sanitize the HTML it produces before you put it on the page, because the reply isn't text your page wrote.

## Try it from a terminal

The API reads the `Origin` header a browser sends. Set it yourself to an allowed website to try the API with `curl`:

```bash
API=https://app.uare.ai/api/v1/public/ai/your-handle

TOKEN=$(curl -s -X POST "$API/sessions" -H "Origin: http://localhost:3000" | sed -E 's/.*"token":"([^"]+)".*/\1/')

curl -N -X POST "$API/chat" \
  -H "Origin: http://localhost:3000" \
  -H "X-Uare-Session: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hi! Who are you?"}'
```

The allowed-websites list decides which sites can show your AI. It isn't a password: anyone can set an `Origin` header outside a browser, so it can't make a conversation private.

## Bring back a conversation

A session lasts up to two hours, and ends after an hour without messages. To show a visitor's conversation again after a page reload, keep the session and token, for example in `sessionStorage`, and load the messages:

```js
const { messages } = await fetch(`${API}/sessions/${sessionId}/messages`, {
  headers: { "x-uare-session": token },
}).then((res) => res.json());
```

They come back as AI SDK `UIMessage` objects, ready for `useChat({ id: sessionId, messages, transport })`.

When a session expires, `POST /chat` returns `404 session_expired`: start a new session. Your AI remembers the conversation within a session, not across sessions.

## Limits

- Each conversation includes 10 visitor messages. After that, `POST /chat` returns `429 message_limit_reached` with a `continue_url` to your AI's profile on uare.ai, where the visitor can keep going.
- Regenerating a reply (`"trigger": "regenerate-message"`, `useChat`'s `regenerate()`) uses up a free message, like sending one.
- Messages are limited to 4,000 characters.
- A conversation answers one message at a time. Sending another while a reply is still streaming returns `409 turn_in_progress`.
- Rate limits apply per visitor IP address. They're listed in the [reference](/docs/api/reference#limits), and every response says how many requests are left in its [`RateLimit-*` headers](/docs/api/reference#rate-limit-headers).

## Next steps

- [Public API reference](/docs/api/reference): every endpoint, field, stream part and error.
- [Customize the website widget](/docs/website/customize), if the ready-made chat fits after all.
