# Public API reference

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

```text
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](https://app.uare.ai/chat-with-me?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]`:

```text
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](#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](/docs/mcp/experts#limits) | 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.
