Public API
Public API reference
Endpoints, fields, stream parts, errors and limits of the uare.ai public chat API.
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}
{
"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:
{
"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
X-Uare-Session: <token>
Content-Type: application/jsonSend either a plain message:
{ "text": "What do you help people with?" }or the body the AI SDK's DefaultChatTransport sends, which useChat produces:
{
"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>.
{
"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:
{ "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.
RateLimit-Policy: 30;w=60
RateLimit-Limit: 30
RateLimit-Remaining: 29
RateLimit-Reset: 60RateLimit-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.