Web Chat API
Preview, not live yet. Put your Astral agent on your own website or app: your server sends a signed-in user's message, and the agent's reply streams back.
Web Chat API
Put your Astral agent on your own website or app. Your server sends a signed-in user's message to Astral. The agent answers from what you let it see, and the reply streams back to your page. Your team sees every conversation in the Astral inbox and can step in.
Status: preview. The Web Chat API is not live yet. This page shows the planned contract, so you can plan your integration now. Names, limits and fields can change before launch.
curl -X POST https://www.astralos.ai/api/v1/web-chat/end-users/user_8812/messages -H "Authorization: Bearer $ASTRAL_WEB_CHAT_KEY" -H "Content-Type: application/json" -d '{"text": "Are you open on Friday evening?"}'Why it is different
- Your agent, not a bare model. The reply comes from your workspace's agent, with its memory, its files and its rules.
- One private conversation for each user. The agent sees one user's conversation at a time. It never sees another user's conversation or memory.
- Your team stays in the loop. Every conversation appears in the Astral inbox. A team member can take over, and the agent stops replying until they hand it back.
- Safe by default. A channel key reaches one channel only. The agent never deletes, shares, invites, grants a credential or changes billing.
How it works
User (signed in to your site) │ types a question ▼Your server │ POST /api/v1/web-chat/end-users/{end_user_id}/messages │ Authorization: Bearer astral_wc_... │ { text, end_user, context } ▼Astral ├── finds or makes the user's contact and conversation ├── runs the channel's agent on this one conversation └── streams the reply back (or returns it as JSON) ▼Your server shows the reply to the user.Your team sees the same conversation in the Astral inbox.- Your server calls the API. The browser never does.
- Your server signs the user in. Astral trusts the
end_user_idthat your server sends.
Terms
| Term | Meaning |
|---|---|
| Web channel | The Astral object for one website or app. It belongs to one workspace and one agent. |
| Channel key | The secret that your server sends. It reaches this one web channel and nothing else. |
| End user | A person who uses your site or app. Your server names each one with an end_user_id. |
| Conversation | The one thread between an end user and the channel. Your team sees it in the Astral inbox. |
| Message | One entry in a conversation. Its author is the end user, the agent, or a team member. |
| Reply | The agent's answer to an end user's message. |
| Context | Data that your server sends with a message, for example the user's account details. The agent reads it for that one reply. |
| Staff mode | A team member has taken the conversation. The agent does not reply until they hand it back. |
Get a channel key
(coming soon) An owner or admin of the workspace opens Settings → Channels → Website or app, and then:
- Names the channel and picks its agent.
- Picks the files and folders that the agent may read for this channel.
- Picks the team member who gets handoffs.
- Sets the limits.
- Copies the key. Astral shows it one time only and stores only a hash.
Use a separate agent for a public channel, not your workspace's main agent. Give it only the folders that the public may read.
Authentication
Send the channel key as a bearer token:
Authorization: Bearer astral_wc_3pQx...- A channel key is
astral_wc_followed by 43 characters. - A channel can have two active keys at the same time. Make the new key, deploy it, then revoke the old key.
- Keep the key on your server. Do not put it in browser code, a mobile app bundle or a repo.
- The key can act for any end user of its channel. It cannot read other channels or anything else in the workspace.
- A personal access token (
astral_...) does not work on these endpoints. A channel key does not work on the other/api/v1endpoints.
Base URL and versions
https://www.astralos.ai/api/v1/web-chatUse this exact host. Every response has the header Astral-Api-Version: 1. Version 1 changes only by added fields, endpoints, event types and error types. Your code must ignore what it does not know.
Response format
Every JSON response has one of two shapes:
{ "ok": true, "data": { } }{ "ok": false, "error": { "type": "rate_limited", "message": "This end user sent 20 messages in the last hour.", "details": { "limit": "end_user_hourly", "retry_after_seconds": 1260 } } }Objects
End user ID
- 1 to 128 characters from
A-Z a-z 0-9 . _ : @ + -. - Use your own stable user id, for example
user_8812. - Do not use a phone number or an email address as the id. The id goes into URLs and logs. Send the phone and the email in the
end_userobject. - URL-encode the id in a path.
Message
{ "id": "6f1c2a9e-1b7d-4c55-9a31-2e0f5d7b8c10", "role": "end_user", "author_name": "Sara A.", "text": "Are you open on Friday evening?", "status": "completed", "client_message_id": "c-20261025-0001", "created_at": "2026-10-25T14:02:11.204Z"}| Field | Meaning |
|---|---|
id | The message id (uuid). A reply's id is also its reply_id. |
role | end_user, agent or staff. |
author_name | The end user's name, the agent's name, or the team member's display name. Can be null. |
text | Agent and staff text is Markdown. End user text is plain text. |
status | running, completed or failed. Always completed for end_user and staff. |
client_message_id | Your idempotency id, on end_user messages only. Can be null. |
created_at | When Astral stored the message (ISO 8601). |
The API never returns tool calls, the agent's internal steps or the context that you sent.
Reply
A reply is a message with role: "agent" and three more fields:
| Field | Meaning |
|---|---|
completed_at | When the reply finished. Null while running. |
handoff | True when the agent gave the conversation to your team in this reply. The conversation is then in staff mode. |
error | On failed: { "type", "message" }. Otherwise null. |
Conversation
{ "end_user_id": "user_8812", "mode": "agent", "created_at": "2026-10-20T09:15:00.000Z", "last_message_at": "2026-10-25T14:02:19.480Z" }mode is agent (the agent replies) or staff (a team member has the conversation, and the agent does not reply).
Context
Context is data from your server about the end user, for one reply. Send a JSON object or a string, up to 16 KB.
{ "account": { "first_name": "Sara", "plan": "Gold", "language": "en" }, "pets": [ { "name": "Max", "species": "dog", "age_years": 6, "last_visit": { "date": "2026-09-14", "reason": "ear infection" } } ]}- The agent reads the context as data, never as instructions. Text in the context that tells the agent to do something has no effect.
- Only the context of the newest message goes to the agent. Send the full, current context with each message.
- Astral keeps the context with the message for audit, and your team can see it. The agent's memory does not learn from it.
- Send only data about this end user.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /channel | Shows the web channel of this key. |
POST | /end-users/{end_user_id}/messages | Sends an end user's message and gets the agent's reply. |
GET | /end-users/{end_user_id}/messages | Reads the end user's conversation. |
GET | /end-users/{end_user_id} | Reads the conversation state. |
GET | /replies/{reply_id} | Reads one reply. Use it after a stream drops. |
DELETE | /end-users/{end_user_id} | Erases the end user's conversation, contact and memory. |
GET /channel
Shows the web channel of the key. Use it to test the key.
{ "ok": true, "data": { "channel": { "id": "3d2b9f0e-5a41-4c8e-9b7a-1f2e3d4c5b6a", "name": "Customer portal", "status": "active", "agent": { "id": "a1b2c3d4-0000-4000-8000-000000000001", "name": "Clinic Assistant" }, "limits": { "end_user_messages_per_hour": 20, "end_user_messages_per_day": 100, "channel_messages_per_minute": 60, "daily_credits": 1000 }, "key": { "id": "9c8b7a6f-0000-4000-8000-000000000002", "prefix": "astral_wc_3pQ", "last_used_at": "2026-10-25T14:02:11Z" } } }}status is active or paused. A paused channel refuses new messages.
POST /end-users/{end_user_id}/messages
Sends one end user message. Astral stores it, runs the agent and returns the reply.
| Body field | Required | Meaning |
|---|---|---|
text | yes | The end user's message, 1 to 4,000 characters. |
end_user | no | { "name", "email", "phone", "locale" }, all optional. Astral updates the contact when a value changes. A missing value never clears a stored value. phone is E.164. |
context | no | An object or a string. See Context above. |
client_message_id | no | Your id for this message, 1 to 64 characters. A retry with the same id returns the first result and does not run the agent again. |
wait_seconds | no | JSON mode only: how long to wait for the reply, 0 to 60. The default is 60. |
The Accept header picks the response mode:
Accept | Response |
|---|---|
text/event-stream | Server-sent events while the agent writes. See Stream events below. |
| anything else | One JSON response, after the reply finishes or wait_seconds ends. |
The reply finished in time (HTTP 200):
{ "ok": true, "data": { "message": { "id": "6f1c...", "role": "end_user", "text": "Are you open on Friday evening?", "status": "completed" }, "reply": { "id": "0b8e...", "role": "agent", "author_name": "Clinic Assistant", "text": "Yes, we are open until 21:00 on Friday.", "status": "completed", "handoff": false }, "conversation": { "end_user_id": "user_8812", "mode": "agent" } }}- The reply did not finish in
wait_seconds: HTTP 202, andreply.statusisrunning. The agent continues. Read the reply later withGET /replies/{reply_id}. - The conversation is in staff mode: HTTP 200 and
replyis null. Astral stores the message, and your team sees it.
Rules:
- One reply at a time for each end user. A send while a reply is
runningfails with409 conflict, anddetails.reply_idnames the running reply. Disable the send button until the reply finishes. - The reply continues when your connection drops. Nothing cancels it.
- A reply that runs longer than 5 minutes fails with
upstream_error. - Each reply costs workspace credits at the agent's normal model rates.
Stream events
With Accept: text/event-stream, the response is a stream of server-sent events. Each event has a name and one JSON data line.
event: message.accepteddata: {"message":{"id":"6f1c...","role":"end_user","status":"completed"},"reply_id":"0b8e...","conversation":{"end_user_id":"user_8812","mode":"agent"}}
event: reply.deltadata: {"reply_id":"0b8e...","text":"Yes, we are open "}
event: reply.deltadata: {"reply_id":"0b8e...","text":"until 21:00 on Friday."}
event: reply.completeddata: {"reply":{"id":"0b8e...","role":"agent","status":"completed","text":"Yes, we are open until 21:00 on Friday.","handoff":false}}| Event | When | data |
|---|---|---|
message.accepted | Astral stored the message. Always the first event. | { message, reply_id, conversation }. In staff mode reply_id is null and the stream ends. |
reply.delta | The agent wrote more text. | { reply_id, text }. Add text to what you have. |
reply.completed | The reply finished. The last event. | { reply }. Show reply.text, the full final text. |
reply.failed | The reply failed. The last event. | { reply_id, error: { type, message } } |
- Astral sends a
: keepalivecomment line every 15 seconds. - An error before
message.accepted(bad key, bad body, a limit) comes as a normal JSON error response, not as a stream. - If the stream drops, read the reply with
GET /replies/{reply_id}. - Ignore event types that you do not know.
GET /end-users/{end_user_id}/messages
Returns the end user's conversation, newest first. Use it to draw the chat when the page opens, and to get team replies.
| Query | Meaning |
|---|---|
limit | 1 to 100. The default is 30. |
before | A cursor from next_before. Gets older messages. |
after | A message id. Gets only newer messages, oldest first. Use it to poll for team replies. |
The response is { conversation, messages, next_before }. An end user with no conversation gets 404 not_found. While the user has the page open, poll with after every 10 seconds to get team replies.
GET /end-users/{end_user_id}
Returns { conversation }, without messages.
GET /replies/{reply_id}
Returns { reply }. While status is running, text holds the text so far. Poll every 2 seconds until the status is completed or failed. A reply of another channel gets 404 not_found.
DELETE /end-users/{end_user_id}
Erases the end user from this channel: the conversation, its messages, the contact and the agent's memory about this user. Use it when a user deletes their account or asks you to erase their data. Returns { "erased": true, "messages": 14 }.
- The erase is permanent.
- A running reply stops first.
- Astral keeps an audit record with the channel, the end user id and the time, but no message text.
- A later message from the same
end_user_idstarts a new, empty conversation.
Errors
| HTTP | error.type | When |
|---|---|---|
| 400 | invalid_input | The body or a parameter is not valid. details.field names it. |
| 401 | auth_required | No Authorization header. |
| 401 | auth_failed | The key is not known, or it is revoked. |
| 403 | permission_denied | details.reason is channel_paused, credits_exhausted or agent_unavailable. |
| 404 | not_found | No such end user, reply or conversation in this channel. |
| 409 | conflict | A reply is still running for this end user. details.reply_id names it. |
| 429 | rate_limited | details.limit is end_user_hourly, end_user_daily, channel_minute or daily_credits. details.retry_after_seconds says when to try again. |
| 502 | upstream_error | The model or Astral failed. Retry with the same client_message_id. |
Retry 429 after retry_after_seconds. Retry 502 with backoff. Do not retry 400, 401, 403, 404 or 409 without a change.
Limits
| Limit | Default |
|---|---|
| Messages for each end user, each hour | 20 |
| Messages for each end user, each day | 100 |
| Messages for the channel, each minute | 60 |
| Credits for the channel, each day | 1,000 (US$10) |
Message text | 4,000 characters |
context | 16 KB |
| Reply run time | 5 minutes |
An admin can change the first four limits for each channel.
What your server must do
- Sign the user in before you call the API. Send only that person's
end_user_id. - Send only that user's data in
context. - Keep the channel key on the server.
- Tell users that an AI agent writes the replies, and that your team can read the conversation.
- Call
DELETE /end-users/{end_user_id}when a user deletes their account.
Example: a clinic's customer portal
A vet clinic's portal signs pet owners in. One portal page lets them ask the clinic's agent about their pets. This Next.js route checks the portal session, adds the owner's pets as context, and streams the reply to the browser. getPortalSession and loadPets stand for the portal's own code.
// app/api/assistant/route.tsimport { getPortalSession } from "@/lib/portal-session"import { loadPets } from "@/lib/pets"
export async function POST(request: Request) { const session = await getPortalSession(request) if (!session) return new Response("Sign in first.", { status: 401 })
const { text, clientMessageId } = await request.json() const pets = await loadPets(session.userId)
const upstream = await fetch( `https://www.astralos.ai/api/v1/web-chat/end-users/${encodeURIComponent(session.userId)}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.ASTRAL_WEB_CHAT_KEY}`, "Content-Type": "application/json", Accept: "text/event-stream", }, body: JSON.stringify({ text, client_message_id: clientMessageId, end_user: { name: session.name }, context: { pets }, }), } )
// An error before the stream starts comes as JSON with its own status. if (!upstream.headers.get("content-type")?.startsWith("text/event-stream")) { return new Response(await upstream.text(), { status: upstream.status, headers: { "Content-Type": "application/json" } }) } return new Response(upstream.body, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-store" } })}The page adds each reply.delta to the answer bubble, and on reply.completed it shows reply.text. When the page opens, a second route returns GET /end-users/{id}/messages.
Coming later
- Webhooks. Astral posts team replies and mode changes to your URL, signed with HMAC-SHA256.
- Browser tokens and a chat widget. A short-lived token for one user, so the browser can talk to Astral directly, and a script tag that draws a chat panel.
- Attachments. Photos and files in a user's message.
- Site actions. The agent calls actions on your server, for example to book an appointment.
- More conversations for each user.
Related
- Astral CLI: the other public API, for tools and access tokens.
- Contacts: how Astral keeps one contact for each person who writes in.
- Approvals: what happens when an action needs a person.