Public API overview
The visitor-facing API used by the widget: endpoints, authentication, errors, rate limits and the polling protocol. Assistants use a separate agent API.
Last updated:
The widget talks to SiteTrellis through a small, versioned JSON API. This page describes that visitor-facing API: what the endpoints are, how requests are authenticated, how errors look, how polling works and what limits apply. It is useful if you want to understand what the widget does, debug requests in your browser's developer tools, or plan a custom chat interface.
Base URL and format#
All endpoints live under https://api.sitetrellis.com/v1. Requests with a body must be POST with Content-Type: application/json and a JSON object (not an array) of at most 64 KB. Responses are JSON. Every response includes an X-Request-Id header, the same id that appears in error bodies: quote it when you contact support.
Authentication#
These endpoints are public, so they do not use an API key. Access is controlled by three things:
- The site's public id (
site_...), sent as?site=on the query string or assitein a JSON body. It identifies which site is being used and is not secret. - The origin allow-list. Browsers send an
Originheader; if it is not in the site's allowed origins the API refuses withorigin_not_allowed, and cross-origin (CORS) headers are only returned for allowed origins. If the list is empty any origin is accepted (setup mode). See allowed origins. - A signed visitor token for everything that touches a conversation.
POST /v1/chat/sessionreturns a token; send it back on every later call, either in theX-ST-Tokenheader or asAuthorization: Bearer <token>. The token is an opaque string signed by SiteTrellis that binds the site, the conversation and the visitor, so a visitor can never reach another visitor's or another customer's conversation.
Tokens last 24 hours and are refreshed automatically: when one is more than twelve hours old, the next response includes a new token and expires_at which the client should adopt. An expired token can still be exchanged for a new one by calling POST /v1/chat/session again for up to 30 days, which resumes the same conversation if it is still open (or recently ended).
Endpoints#
| Method and path | Purpose | Token |
|---|---|---|
GET /v1/widget/config?site=site_xxx | Widget settings plus the agent's public presentation: name, greeting, starters, pre-chat form, business-hours status. Cached with an ETag; send If-None-Match to get 304. | No |
POST /v1/chat/session | Start or resume a conversation. Body may include site, page_url, page_title, attributes and new (force a fresh conversation). Returns the token, conversation state, any existing messages and pre-chat status. | Optional (to resume) |
POST /v1/chat/messages | Send a visitor message: {"text": "...", "client_id": "..."}. The AI turn runs before the response returns when possible. | Yes |
GET /v1/chat/messages?after=<id> | Poll for new messages since a message id. | Yes |
POST /v1/chat/forms | Submit a form: {"form": "prechat" | "lead" | "escalation", "fields": {...}}. Validated against the agent's configured fields. | Yes |
POST /v1/chat/feedback | Thumbs up or down on a reply: {"message_id": 123, "rating": 1 | -1}. | Yes |
POST /v1/chat/end | The visitor ends the conversation. | Yes |
POST /v1/chat/typing | Tell the server the visitor is typing (shown to a human operator): {"typing": true}. | Yes |
POST /v1/chat/attributes | Merge visitor attributes: {"attributes": {"name": "...", "plan": "..."}}. Used by the JavaScript API's identify and setAttributes. | Yes |
POST /v1/chat/transcript | Email the visitor a copy of the conversation: {"email": "..."}. | Yes |
Message shape#
Messages returned by the API always look like this:
{
"id": 1042,
"role": "assistant",
"content": "Standard delivery takes 2-4 working days.",
"content_type": "text",
"created_at": "2026-10-07T14:03:22+00:00",
"meta": {}
}
roleisvisitor,assistant,operator(a human teammate;operator_nameis included) orsystem(a small notice such as "Sam joined the chat"). Tool activity is never exposed.content_typeistext(light markdown) orform, wheremeta.formdescribes an inline form to show (id,title,fields).metamay also carry alinkorbuttonsfor rich replies.feedbackshows the visitor's rating once given.
Sending a message#
curl -X POST "https://api.sitetrellis.com/v1/chat/messages?site=site_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Origin: https://www.example.com" \
-H "X-ST-Token: $TOKEN" \
-d '{"text": "How long does delivery take?", "client_id": "m-20261007-0001"}'
The response contains the stored visitor message, any replies that are ready, a status, the conversation mode and a last_id cursor. status is done (the AI replied), human (a person is handling the chat), escalated (a handoff was just started), or closed; a pending status is reserved for replies that are not ready yet, in which case you should poll for them. The AI turn normally runs before the response is returned, so a call can take several seconds (up to roughly a minute in the worst case): use a generous client timeout. Supplying client_id (6 to 64 letters, digits, _ or -) makes the call idempotent: if the request is retried after a network error, the same message is not stored twice.
Polling protocol#
SiteTrellis does not use WebSockets or server-sent events. The client polls:
- Keep the highest message id you have seen (
last_id). - Call
GET /v1/chat/messages?after=<last_id>. The reply lists only newer messages, in order, together withmode(ai,humanorclosed),status(openorclosed),operator_typing(best effort, short lived) and a newlast_id. - Wait
retry_after_msmilliseconds before the next poll. The server tunes this: about 3 seconds while the AI is active, about 2 seconds with a human, and up to 30 seconds once closed. Never poll faster than the hint. - Back off when the tab is hidden or the visitor is idle, and speed up again after activity. Stop polling when the conversation is closed.
async function poll(state) {
const url = `https://api.sitetrellis.com/v1/chat/messages?site=${state.site}&after=${state.lastId}`;
const res = await fetch(url, { headers: { 'X-ST-Token': state.token } });
if (res.status === 401) return resume(state); // token expired: call /chat/session again
const data = await res.json();
if (data.token) state.token = data.token; // refreshed token
for (const m of data.messages) render(m);
state.lastId = data.last_id;
if (data.status !== 'closed') {
setTimeout(() => poll(state), data.retry_after_ms);
}
}
Errors#
Errors use a consistent shape and a meaningful HTTP status:
{
"error": {
"code": "origin_not_allowed",
"message": "This website is not allowed to use this chat. Add the domain to the site's allowed origins in the SiteTrellis dashboard.",
"request_id": "9f31c2a8b7d4"
}
}
Validation failures add a fields object with one message per field, and rate-limit errors add retry_after_ms and a Retry-After header (in seconds). Code on error.code, not on the message text, which may change.
| Code | Status | Meaning |
|---|---|---|
invalid_json, unsupported_media_type, payload_too_large | 400, 415, 413 | The body is not a JSON object, is not sent as JSON, or exceeds 64 KB. |
site_not_found | 404 | Unknown site id, or the account is suspended. |
origin_not_allowed | 403 | The page's origin is not in the site's allowed origins. |
token_missing, token_invalid, token_expired | 401 | Missing, malformed or expired session token. Start or resume with /chat/session. |
site_mismatch | 403 | The token belongs to a different site. |
conversation_not_found | 404 | The conversation does not exist, was deleted, or the account is not active. |
validation_failed | 422 | One or more fields are invalid; see error.fields. |
too_long | 413 or 422 | The message is longer than the agent allows (default 2,000 characters) or than the API maximum. |
prechat_required | 409 | The agent requires the pre-chat form before the first message. |
busy | 409 | The assistant is still replying to the previous message. Wait and retry. |
closed | 409 | The conversation has ended. Start a new one. |
plan_limit | 403 | The owner's plan limit has been reached. See Billing and plans. |
no_agent, unavailable | 404, 403 | No published, enabled agent is available for the site, or the account is not active. |
rate_limited | 429 | Too many requests. Wait for retry_after_ms. |
server_error | 5xx | Something failed on our side. Retry shortly and report the request_id if it persists. |
Rate limits#
Limits apply per visitor conversation, per IP address and per site, and are intentionally generous for human typing speed. At the time of writing, approximately: 30 messages per minute per conversation, 180 polls per minute per conversation, 30 new sessions per minute per IP, and 3 transcript emails per conversation per hour. The agent can also set its own per-minute message limit (default 12). Values may be adjusted; always honour Retry-After and retry_after_ms rather than hard-coding numbers.
CORS#
Browsers send a preflight OPTIONS request before cross-origin POST calls. The API answers it with the allowed methods (GET, POST, OPTIONS) and headers (Content-Type, X-ST-Token, Authorization) only when the requesting origin is on the site's allow-list. Server-to-server calls without an Origin header are not subject to CORS but are still rate limited and token-checked.
What this API is not#
- It is not a way to read other people's conversations: you can only reach the conversation your own token was issued for.
- It does not expose configuration, keys or analytics. Those stay in the dashboard.
- It is not the interface for your own scripts and assistants. For those, create an API key or connect an MCP assistant with permissions you choose: see Connect Claude, ChatGPT and API keys.
See also: Widget JavaScript API, Webhooks, Security and privacy.