Skip to content
Docs

Search the docs

Find a page or a section

Public API

Public API reference

Endpoints, fields, stream parts, errors and limits of the uare.ai public chat API.

View as Markdown
https://app.uare.ai/api/v1/public/ai/{handle}

{handle} is the last part of your AI's public profile address. The API answers web pages on the websites your AI's owner allows in Settings → Website. It needs no API key: a visitor's session token, sent in an X-Uare-Session header, authorizes their own conversation and nothing else.

Endpoints

Method Path Does
GET /api/v1/public/ai/{handle} Returns the AI's public card
POST /api/v1/public/ai/{handle}/sessions Starts a conversation and returns its token
POST /api/v1/public/ai/{handle}/chat Sends a message and streams the reply
GET /api/v1/public/ai/{handle}/sessions/{session_id}/messages Returns the conversation so far

Get the AI

GET /api/v1/public/ai/{handle}

JSON
{
  "ai": {
    "handle": "your-handle",
    "name": "Raj",
    "avatar_url": "https://…/avatar.png",
    "bio": "Makes hard ideas simple.",
    "profile_url": "https://app.uare.ai/p/your-handle"
  },
  "quota": { "limit": 10 }
}

quota.limit is how many messages a visitor can send in one conversation. Use this call to show your AI's name and picture before a visitor starts typing. It starts nothing.

Like every endpoint here, it answers only a request with an Origin header naming an allowed website, which browsers add for you. Code on your server, such as a Next.js server component or server-side rendering, sends no Origin and gets 403 origin_required. Fetch the card from the page instead, or send one of the allowed websites as the Origin header from your server.

Start a session

POST /api/v1/public/ai/{handle}/sessions

The body is optional:

Field Type Description
language string The visitor's language, as a tag such as en or fr-CA. Defaults to en. It's kept with the session; your AI picks the language of its replies itself.

Returns 201:

JSON
{
  "session_id": "8f2c…",
  "token": "eyJhbGciOiJIUzI1NiJ9…",
  "expires_at": "2026-10-16T21:04:12.000Z",
  "quota": { "limit": 10, "remaining": 10 },
  "ai": { "handle": "your-handle", "name": "Raj", "avatar_url": "https://…", "bio": "…", "profile_url": "…" }
}

Start a session when a visitor begins chatting, not on every page view. The token belongs to that one visitor's conversation: don't share it between visitors. It expires at expires_at, two hours after it was issued. The session can end sooner, after an hour without messages, and then POST /chat returns 404 session_expired.

Send a message

POST /api/v1/public/ai/{handle}/chat

HTTP
X-Uare-Session: <token>
Content-Type: application/json

Send either a plain message:

JSON
{ "text": "What do you help people with?" }

or the body the AI SDK's DefaultChatTransport sends, which useChat produces:

JSON
{
  "id": "8f2c…",
  "trigger": "submit-message",
  "messages": [
    { "id": "m1", "role": "user", "parts": [{ "type": "text", "text": "What do you help people with?" }] }
  ]
}

The API reads only the newest user message. The conversation's earlier turns come from the server, so a page can't rewrite them. With "trigger": "regenerate-message", your AI answers the last message again, replacing its previous reply. A regenerated reply uses up one of the conversation's free messages, like a new message.

The response is text/event-stream, with the header x-vercel-ai-ui-message-stream: v1. Each event is one data: line holding a JSON part, and the stream ends with data: [DONE]:

data: {"type":"start","messageId":"msg_4b1c…"}

data: {"type":"data-quota","data":{"limit":10,"remaining":9},"transient":true}

data: {"type":"text-start","id":"txt_9a…"}

data: {"type":"text-delta","id":"txt_9a…","delta":"I help people "}

data: {"type":"text-delta","id":"txt_9a…","delta":"make hard ideas simple."}

data: {"type":"text-end","id":"txt_9a…"}

data: {"type":"data-suggestions","data":{"items":[{"label":"What's your approach?","prompt":"What's your approach?"}]}}

data: {"type":"finish"}

data: [DONE]

Stream parts

Part Data Meaning
start messageId The reply begins.
data-quota { limit, remaining } Messages left in this conversation after this one. Transient: not stored in the message.
data-status { label } What your AI is doing, such as Thinking back.... Transient.
text-start, text-delta, text-end id, delta The reply's text, in order. Join the deltas.
data-suggestions { items: [{ label, prompt }] } Follow-ups to offer the visitor. Send prompt when one is picked.
data-limit { limit, continue_url } The free messages are used up. Followed by an error part.
error errorText The reply failed. The text is safe to show the visitor.
finish The reply is complete.

Ignore part types you don't recognise: new ones may be added.

Get the conversation

GET /api/v1/public/ai/{handle}/sessions/{session_id}/messages, with X-Uare-Session: <token>.

JSON
{
  "messages": [
    { "id": "8f2c…:0", "role": "user", "parts": [{ "type": "text", "text": "What do you help people with?" }] },
    { "id": "8f2c…:1", "role": "assistant", "parts": [{ "type": "text", "text": "I help people make hard ideas simple." }] }
  ]
}

The messages are AI SDK UIMessage objects, so useChat({ messages }) can show them again after a reload.

Errors

Errors before a stream starts come back as JSON with a matching status:

JSON
{ "error": { "code": "origin_not_allowed", "message": "https://example.com isn't on Raj's list of allowed websites. …" } }
Status code What it means
400 invalid_request The body isn't valid: an empty message, one over 4,000 characters, or bad JSON.
401 session_token_required No X-Uare-Session header.
401 invalid_session_token The token is malformed or expired. Start a new session.
403 origin_required The request has no Origin header: it came from a server or a terminal, not a web page. Call the API from a web page, or set Origin to an allowed website.
403 origin_not_allowed The page's website isn't on the AI's list of allowed websites.
403 session_mismatch The token belongs to another session or another AI.
404 ai_not_found No public AI answers to this handle. Its profile may be private.
404 session_expired The session ended. Start a new one.
409 turn_in_progress A reply is still streaming in this conversation. Send the next message when it has finished.
429 message_limit_reached The conversation's free messages are used up. The body adds limit and continue_url.
429 Too many requests from this address. Wait the seconds in Retry-After, then retry. See Rate limit headers.
503 chat_unavailable The AI can't take visitor chats right now.

Errors after the stream starts arrive as an error part, and the stream ends.

Limits

Limit Value
Messages per conversation 10 (quota.limit)
Message length 4,000 characters
New sessions 20 per minute per IP address
Messages 30 per minute per IP address
All public endpoints, apart from the Experts MCP server 120 requests per minute per IP address
Session lifetime 2 hours, or 1 hour without messages
Allowed websites per AI 20

Regenerating a reply counts toward the messages per conversation.

Rate limit headers

Every response carries RateLimit-* headers for the per-minute limit that applies to it: 20 for POST /sessions, 30 for POST /chat, and 120 for the other endpoints.

HTTP
RateLimit-Policy: 30;w=60
RateLimit-Limit: 30
RateLimit-Remaining: 29
RateLimit-Reset: 60

RateLimit-Remaining is how many more of those requests this IP address can make in the current minute, and RateLimit-Reset how many seconds to wait, 60 at most, for the count to start over. A 429 adds Retry-After, in seconds.

Browsers don't show a page on another website the RateLimit-* headers: read those from your server or a terminal. A page can read Retry-After on a 429 from POST /sessions or POST /chat. Past the 120-request limit, the 429 comes without CORS headers, so the page sees a failed request instead: wait a minute before retrying.

CORS

The API answers cross-origin requests from any page and sends no cookies. Preflight requests always succeed, allowing the Content-Type and X-Uare-Session headers. The real request then checks the page's Origin against the AI's allowed websites, and a refused page gets a readable 403 explaining why.