Browse all docs

Webhooks and API

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.

Scope The public API is designed for the official widget. Creating sites, agents or keys is done in the dashboard. Assistants such as Claude or ChatGPT, scripts and apps use a separate, permission-limited interface: see connect an AI assistant or API key. For event delivery to your own systems use webhooks. Field details of the endpoints below may be extended over time; treat unknown response fields as optional.

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:

  1. The site's public id (site_...), sent as ?site= on the query string or as site in a JSON body. It identifies which site is being used and is not secret.
  2. The origin allow-list. Browsers send an Origin header; if it is not in the site's allowed origins the API refuses with origin_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.
  3. A signed visitor token for everything that touches a conversation. POST /v1/chat/session returns a token; send it back on every later call, either in the X-ST-Token header or as Authorization: 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 pathPurposeToken
GET /v1/widget/config?site=site_xxxWidget 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/sessionStart 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/messagesSend 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/formsSubmit a form: {"form": "prechat" | "lead" | "escalation", "fields": {...}}. Validated against the agent's configured fields.Yes
POST /v1/chat/feedbackThumbs up or down on a reply: {"message_id": 123, "rating": 1 | -1}.Yes
POST /v1/chat/endThe visitor ends the conversation.Yes
POST /v1/chat/typingTell the server the visitor is typing (shown to a human operator): {"typing": true}.Yes
POST /v1/chat/attributesMerge visitor attributes: {"attributes": {"name": "...", "plan": "..."}}. Used by the JavaScript API's identify and setAttributes.Yes
POST /v1/chat/transcriptEmail 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": {}
}
  • role is visitor, assistant, operator (a human teammate; operator_name is included) or system (a small notice such as "Sam joined the chat"). Tool activity is never exposed.
  • content_type is text (light markdown) or form, where meta.form describes an inline form to show (id, title, fields).
  • meta may also carry a link or buttons for rich replies. feedback shows 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:

  1. Keep the highest message id you have seen (last_id).
  2. Call GET /v1/chat/messages?after=<last_id>. The reply lists only newer messages, in order, together with mode (ai, human or closed), status (open or closed), operator_typing (best effort, short lived) and a new last_id.
  3. Wait retry_after_ms milliseconds 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.
  4. 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.

CodeStatusMeaning
invalid_json, unsupported_media_type, payload_too_large400, 415, 413The body is not a JSON object, is not sent as JSON, or exceeds 64 KB.
site_not_found404Unknown site id, or the account is suspended.
origin_not_allowed403The page's origin is not in the site's allowed origins.
token_missing, token_invalid, token_expired401Missing, malformed or expired session token. Start or resume with /chat/session.
site_mismatch403The token belongs to a different site.
conversation_not_found404The conversation does not exist, was deleted, or the account is not active.
validation_failed422One or more fields are invalid; see error.fields.
too_long413 or 422The message is longer than the agent allows (default 2,000 characters) or than the API maximum.
prechat_required409The agent requires the pre-chat form before the first message.
busy409The assistant is still replying to the previous message. Wait and retry.
closed409The conversation has ended. Start a new one.
plan_limit403The owner's plan limit has been reached. See Billing and plans.
no_agent, unavailable404, 403No published, enabled agent is available for the site, or the account is not active.
rate_limited429Too many requests. Wait for retry_after_ms.
server_error5xxSomething 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.