Both endpoints accept a JSON or form-encoded POST body, return JSON, and are rate limited per client IP address: 10 requests per 60 seconds by default, shared between the two endpoints. The client IP is taken from the CF-Connecting-IP header, then X-Forwarded-For, then the remote address.
The search endpoint is public. The chat endpoint runs with the configuration of the place the chat lives, which the server looks up: a Chatbot page block, named by block_id, or the floating widget. The system prompt is never taken from the request.
POST /chatbot/api/chat
Request from a Chatbot page block:
{
"question": "What Roman artefacts are in the collection?",
"block_id": 42
}
Request from the floating widget (no block_id):
{
"question": "What Roman artefacts are in the collection?",
"site_id": 1,
"site_slug": "my-site"
}
| Field | Required | Description |
|---|---|---|
question |
yes | 1 to 1000 characters. |
block_id |
for blocks | The id of the Chatbot page block the chat is on. The server uses that block's System prompt override (or the module's default prompt), its Restrict to item sets list and its page's site, and enforces its Allow anonymous users option. The visitor must be able to see the block's page. |
site_id |
no | Widget requests only: use evidence only from resources belonging to this site. Omit to search across all sites. Ignored with block_id. |
site_slug |
no | Widget requests only: rewrite source URLs to /s/{site_slug}/.... Ignored with block_id. |
item_set_ids |
no | Widget requests only: array of item set ids. Only chunks of items in at least one of these sets, and of those items' media, are used as evidence; blog posts and site pages are excluded when this is sent. Non-positive and non-numeric values are ignored; an empty array means no restriction. Ignored with block_id, which uses the block's own list. |
Requests without block_id use the module's default system prompt and are accepted only while Enable global chat widget on all public pages is on. A system_prompt field, sent by versions before 1.5.1, is ignored.
Response (200):
{
"answer_html": "<p>The collection holds Roman pottery<sup><a href=\"#chatbot-source-1\" class=\"chatbot-citation\">[1]</a></sup> ...</p>",
"sources": [
{
"citation_num": 1,
"title": "Roman Pottery Shard",
"url": "/s/my-site/item/42",
"resource_type": "item",
"resource_id": 42,
"snippet": "Roman Pottery Shard\n\nA fragment of ..."
}
]
}
answer_html is escaped paragraph HTML in which [N] markers have become links to #chatbot-source-N. sources contains only the evidence items the model cited, in citation order. resource_type is item, media, blog_post or site_page. Snippets are the first 200 characters of the matching chunk.
POST /chatbot/api/search
Request:
{
"query": "agricultural buildings",
"page": 1,
"per_page": 10,
"site_id": 1,
"site_slug": "my-site",
"item_set_ids": [3, 5]
}
| Field | Required | Description |
|---|---|---|
query |
yes | 1 to 500 characters. |
page |
no | Default 1. |
per_page |
no | Default 10, maximum 50. |
site_id, site_slug |
no | As for chat. |
item_set_ids |
no | As for chat: only items in these sets and their media are returned; blog posts and site pages are excluded. |
Response (200):
{
"results": [
{
"resource_type": "item",
"resource_id": 42,
"title": "Threshing Barn",
"url": "/s/my-site/item/42",
"distance": 0.2134,
"snippet": "Threshing Barn\n\nTimber-framed barn ..."
}
],
"total": 14,
"page": 1,
"per_page": 10
}
distance is the cosine distance, where 0 is identical. Results are one per resource URL, sorted by distance. total is the number of distinct resources among the fetched chunks (at most 100, or 150 with a site filter), not the size of the collection. Snippets are up to 300 characters.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | {"error": "Question is required."} and similar |
Missing or over-length question or query. |
| 403 | {"error": "Please log in to use the chat."} |
Chat: the block does not allow anonymous use and the visitor is not logged in. |
| 403 | {"error": "Chat is not available."} |
Chat: block_id is not a Chatbot block, or is on a page the visitor may not see; or no block_id was sent while the widget is off. |
| 405 | {"error": "Method not allowed."} |
The request was not a POST. |
| 429 | {"error": "Rate limit exceeded. Please try again shortly."} |
The per-IP limit was hit. |
| 503 | {"error": "The chat service is temporarily unavailable. Please try again later."} (search: "Search is temporarily unavailable. Please try again later.") |
Turso unavailable or its index needs rebuilding, an OpenAI error, or another runtime failure. The underlying error is written to the module log (chat.error, search.error), never sent to the visitor. |
| 500 | {"error": "An unexpected error occurred. Please try again."} |
Anything else. |
Example
curl -X POST https://example.org/chatbot/api/chat \
-H 'Content-Type: application/json' \
-d '{"question": "Who built the mill?", "block_id": 42}'