Skip to content
Docs

Search the docs

Find a page or a section

Public API

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.

View as Markdown

The website widget 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.
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, 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 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:

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, 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 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.

<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>

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:

JavaScript
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, and every response says how many requests are left in its RateLimit-* headers.

Next steps