Site Assistant
An AI chat assistant for the visitors of your site, using your own AI key (OpenAI, Anthropic, Groq, OpenRouter, Mistral or Cloudflare Workers AI). You set the model, instructions, topic and limits; visitors only send their question and get the answer. Daily cap protects your AI budget. Optional realtime voice (paid plans): visitors talk to the assistant in the browser via OpenAI Realtime, with your key kept server-side and every call ended after max_minutes.
Try it with your AI assistant
Once WebsitePublisher is connected to your AI assistant, paste one of these into the chat. Replace the parts in [brackets] with your own details.
@WebsitePublisher add an AI chat assistant to my site that answers questions about my services@WebsitePublisher limit the chat assistant on my site to questions about [your topic] and cap it at [50] answers a dayWith WebsitePublisher, add an AI chat assistant to my site that answers questions about my servicesWith WebsitePublisher, limit the chat assistant on my site to questions about [your topic] and cap it at [50] answers a dayWith WebsitePublisher, add an AI chat assistant to my site that answers questions about my servicesWith WebsitePublisher, limit the chat assistant on my site to questions about [your topic] and cap it at [50] answers a dayEndpoints (10)
Create an AI assistant for site visitors. It uses the AI integration of this project, so the key of the chosen provider must be in the vault (openai: openai_api_key, anthropic: aapi_anthropic_key, groq: groq_api_key, openrouter, mistral, cloudflare-workers-ai: see their integration). Visitors can never change the model, instructions or limits. scope sets how broadly it may answer: "strict" (default) only about topic, "related" also briefly about closely related questions, "open" anything. Do NOT put secrets, internal prices or private data in system_prompt: a determined visitor may get the model to reveal it. After creating, call test-assistant once to check it works.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | Short id, lowercase letters, digits, - and _. Example: "help". |
label |
string | ✓ Yes | Human-readable name. |
provider |
string | ✓ Yes | "openai", "anthropic", "groq", "openrouter", "mistral" or "cloudflare-workers-ai". |
model |
string | ✓ Yes | Model id of that provider, e.g. "gpt-4o-mini", "claude-haiku-4-5-20251001", "llama-3.1-8b-instant". |
system_prompt |
string | ✓ Yes | Instructions for the assistant: who it is, what it knows about the site, tone. Max 8000 characters. |
access |
string | ✓ Yes | "anonymous" (any visitor) or "verified" (signed-in visitors or tenants only). No default: choose deliberately. |
scope |
string | No | "strict" (default), "related" or "open". |
topic |
string | No | What the assistant is about, e.g. "our hiking routes, bookings and opening hours". Required unless scope is "open". |
off_topic_reply |
string | No | Fixed reply for questions outside the topic. Empty: the assistant politely says in the visitor's language what it can help with. |
max_output_tokens |
integer | No | Max length of one answer (1-4000). Default 500. |
temperature |
number | No | 0-1, default 0.7. Not applied for providers "openai" and "anthropic" (their newer models refuse it). |
max_input_chars |
integer | No | Max characters per visitor message (1-8000). Default 1000. |
max_history_messages |
integer | No | How many earlier messages are sent along for context (0-40). Default 10. The page sends this history, so a visitor can make up earlier messages; turn on signed_history to leave out answers the assistant never gave, or set 0 so the assistant only sees the current question. |
signed_history |
boolean | No | Default false. true: an earlier answer in history only counts when the page sends back its reply_sig as sig, with the answer exactly as chat returned it; other answers are left out. Stops a visitor from making up what the assistant said. The page must keep reply_sig (see chat). |
per_session_hourly |
integer | No | Max messages per visitor (session, or email when verified) per hour (1-1000). Default 30. |
per_ip_hourly |
integer | No | Anonymous only: max messages per IP address per hour (0 = off, max 1000). Default 60. |
per_ip_daily |
integer | No | Anonymous only: max messages per IP address per day (UTC), 0 = off, max 10000. Default 100 (assistants created before this setting existed have 0). Stops one visitor or script from using up daily_cap on its own. Visitors who share an IP (office, school, mobile network) count together: raise it or set 0 if they get blocked. Over it the visitor gets reason "network_limit". |
daily_cap |
integer | No | Budget brake: max answers per day (UTC) for this assistant, all visitors together (1-100000). Default 500. After that visitors see fallback_message. |
fallback_message |
string | No | What visitors see when a limit is reached or the AI provider fails. Default: "The assistant is not available right now. Please try again later." |
Change an assistant. Only send what changes. Send an empty string for topic or off_topic_reply to clear it. Set is_active false to pause (visitors then see fallback_message).
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | |
label |
string | No | |
provider |
string | No | |
model |
string | No | |
system_prompt |
string | No | |
access |
string | No | |
scope |
string | No | |
topic |
string | No | |
off_topic_reply |
string | No | |
max_output_tokens |
integer | No | |
temperature |
number | No | |
max_input_chars |
integer | No | |
max_history_messages |
integer | No | |
signed_history |
boolean | No | |
per_session_hourly |
integer | No | |
per_ip_hourly |
integer | No | |
per_ip_daily |
integer | No | |
daily_cap |
integer | No | |
fallback_message |
string | No | |
is_active |
boolean | No |
Delete an assistant permanently. Realtime calls that are still open are ended first (counts in realtime_calls). To pause it instead, use update-assistant with is_active false.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes |
All assistants of this project with their settings, today's number of answers against daily_cap, whether the provider key is in the vault, and the platform limits. realtime holds the voice settings (null when not set up), today's calls against daily_sessions and open_calls (calls not yet closed by the platform; a call the visitor ended counts until its scheduled end).
No input parameters required.
Send one test message with exactly the visitor settings (instructions, scope, limits) and see the answer, or the error from the AI provider. Counts towards daily_cap. Use this after create or update.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | |
message |
string | ✓ Yes | |
history |
array | No | Optional earlier messages, same format as chat. |
Turn on realtime voice (speech-to-speech in the browser) for an assistant, or change its settings. Paid plans only. Uses the OpenAI key of this project (vault: openai_api_key); the key never reaches the browser. The assistant's system_prompt, scope and topic become the voice instructions; the platform adds rules for a spoken conversation (short answers, no made-up details). Spoken answers have their own max_output_tokens (default 1024, about 40 seconds of speech; audio counts as tokens), separate from text chat. The first call needs model; later calls only send what changes. Every call is ended automatically after max_minutes, and daily_sessions caps the number of calls per day (UTC). Once connected, a determined visitor can change the instructions over the browser connection, so never put secrets in system_prompt. For the visitor transcript, transcribe_model and its hints (transcribe_prompt, transcribe_keywords, transcribe_languages, transcribe_delay) set the accuracy; a hint the chosen model does not use is left out of the call and listed in transcription_notes. After setting up, call test-realtime once.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | |
model |
string | No | OpenAI realtime model, e.g. "gpt-realtime" or "gpt-realtime-mini". Required the first time. |
voice |
string | No | OpenAI voice, e.g. "marin" (default), "cedar" or "alloy". |
max_minutes |
integer | No | Each call is ended after this many minutes (1-60). Default 10. |
daily_sessions |
integer | No | Budget brake: max calls per day (UTC) for this assistant, all visitors together (1-10000). Default 50. |
max_open_calls |
integer | No | Max calls open at the same time for this assistant (0-1000). 0 = no cap (default). A call counts until its scheduled end (max_minutes) or end-realtime-calls, also when the visitor already left. Over the cap a visitor gets limited with reason "busy". |
per_actor_hourly |
integer | No | Max calls per visitor (session, or email when verified) per hour (1-100). Default 5. |
per_ip_hourly |
integer | No | Anonymous only: max calls per IP address per hour (0 = off, max 1000). Default 10. |
per_ip_daily |
integer | No | Anonymous only: max calls per IP address per day (UTC), 0 = off, max 10000. Default 10 when voice is first set up (voice set up before this setting existed has 0). Stops one visitor or script from using up daily_sessions on its own. Visitors who share an IP count together: raise it or set 0 if they get blocked. Over it the visitor gets reason "network_limit". |
max_output_tokens |
integer | No | Max length of one spoken answer in tokens (1-4096). Default 1024. Audio counts: about 25 tokens per second of speech. |
transcribe_visitor |
boolean | No | true: what the visitor says is transcribed and sent to the page as conversation.item.input_audio_transcription.completed events. Default false. The transcript comes from a separate transcription model (transcribe_model, default gpt-4o-mini-transcribe), not from the voice model, so it can differ from what the assistant understood. Billed by OpenAI on your key at the price of that transcription model. |
transcribe_language |
string | No | Language of the visitors as a two-letter ISO 639-1 code (e.g. "nl"), a hint for transcribe_visitor; without it OpenAI guesses the language per sentence. Empty string clears it. |
transcribe_model |
string | No | Transcription model for transcribe_visitor. Default "gpt-4o-mini-transcribe" (cheapest). "gpt-4o-transcribe" is more accurate, also for less common languages, names and domain words, and costs more. Also accepted: "gpt-4o-mini-transcribe-2025-12-15", "gpt-transcribe", "gpt-live-transcribe", "gpt-realtime-whisper", "whisper-1". Call test-realtime after a change. Empty string = back to the default. |
transcribe_prompt |
string | No | Hint for the transcription with the names and terms your visitors use, e.g. "Names: ELMA, Element Marketing. Terms: CRM, Google Meet." Used with gpt-4o-transcribe, gpt-4o-mini-transcribe and whisper-1 (for whisper-1 a plain list of words). Max 1000 characters. Not shown to visitors, but never put secrets in it. Empty string clears it. |
transcribe_keywords |
array | No | Words or phrases that guide the transcription, e.g. ["ELMA","Element Marketing","CRM"]. Used with gpt-transcribe and gpt-live-transcribe (for the gpt-4o models use transcribe_prompt). Max 100, each max 100 characters. [] clears it. |
transcribe_languages |
array | No | Possible languages of the visitors as ISO 639-1 codes, e.g. ["uk","ru"]. Used with gpt-transcribe and gpt-live-transcribe instead of transcribe_language. Max 10. [] clears it. |
transcribe_delay |
string | No | gpt-realtime-whisper only: how long it waits before it emits text, "minimal", "low", "medium", "high" or "xhigh". Longer can be more accurate, but the text comes later. Empty string clears it. |
noise_reduction |
string | No | "far_field" (default, laptop or phone microphone), "near_field" (headset) or "off". |
turn_detection |
string | No | When the assistant starts to answer. "semantic_auto" (default), "semantic_low", "semantic_medium" or "semantic_high": waits until the visitor has finished a sentence (low waits longest, calmest with background noise). "server_vad": answers after any sound followed by a short silence (fastest, most sensitive to noise). |
enabled |
boolean | No | false pauses realtime voice; text chat keeps working. |
Check that the OpenAI key, model, voice and the other voice settings work for realtime: sends the same session a call gets, without starting a call or using audio. Does not count towards daily_sessions. Returns {ok, model, voice, error, transcription_model, transcription_notes}; transcription_model is null when transcribe_visitor is off.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes |
Emergency stop: end every realtime call of this assistant that is still open. Works on every plan. To stop new calls as well, use set-realtime with enabled false. Returns counts: ended, already_ended, unconfirmed (OpenAI answered 503, which it does for a call the visitor already ended; the call is closed) and failed.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes |
The current visitor asks the assistant a question. Returns {reply, limited, reply_sig}. When limited is true, reply holds the fallback message, reason says why ("daily_limit", "visitor_limit", "network_limit", "inactive", "unavailable") and there is no reply_sig. For a conversation, the page keeps the earlier messages itself and sends them as history; nothing is stored on the server. Keep reply_sig with each answer and send it back as sig in history. With signed_history on, earlier answers without a valid sig are left out, so a visitor cannot make up what the assistant said.
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | |
message |
string | ✓ Yes | The visitor's question. The assistant's own max_input_chars applies. |
history |
array | No | Earlier messages of this conversation, oldest first: [{"role":"user","content":"..."},{"role":"assistant","content":"...","sig":"<reply_sig>"}]. Only user and assistant roles are used. Send each answer exactly as chat returned it, with its reply_sig as sig. |
Start a realtime voice call for the current visitor (set up first with set-realtime). The page creates an RTCPeerConnection with the microphone track and a data channel "oai-events", sends its SDP offer here and gets the SDP answer back: {sdp, max_minutes, limited:false}. Audio then flows directly between the browser and OpenAI; the data channel carries the events: the assistant's words as response.output_audio_transcript.delta/done and, with transcribe_visitor on, what the visitor said as conversation.item.input_audio_transcription.completed. When limited is true there is no sdp: message holds the fallback text and reason says why ("daily_limit", "visitor_limit", "network_limit", "busy", "inactive", "not_enabled", "plan", "unavailable"). sapi.execute resolves to {ok, status, data}: the result is in data.result; a refusal has data.success false and data.error. Example: const pc = new RTCPeerConnection(); const audio = new Audio(); audio.autoplay = true; pc.ontrack = e => { audio.srcObject = e.streams[0]; }; const mic = await navigator.mediaDevices.getUserMedia({audio: {echoCancellation: true, noiseSuppression: true}}); pc.addTrack(mic.getTracks()[0]); pc.createDataChannel("oai-events"); await pc.setLocalDescription(await pc.createOffer()); const res = await sapi.execute("site-assistant", "realtime-call", {assistant_key: "tutor", sdp: pc.localDescription.sdp}); const r = res.data && res.data.success ? res.data.result : null; if (!r || r.limited) { pc.close(); /* show r ? r.message : res.data.error */ } else { await pc.setRemoteDescription({type: "answer", sdp: r.sdp}); } Start the call from a user click (browsers require a gesture for the microphone and audio playback).
| Field | Type | Required | Description |
|---|---|---|---|
assistant_key |
string | ✓ Yes | |
sdp |
string | ✓ Yes | pc.localDescription.sdp after setLocalDescription(await pc.createOffer()). |
MCP Tool Names
When using this integration through an AI assistant (Claude, ChatGPT, Cursor, etc.), the endpoints are available as MCP tools:
| Endpoint | MCP Tool Name |
|---|---|
| create-assistant | site-assistant_create_assistant |
| update-assistant | site-assistant_update_assistant |
| delete-assistant | site-assistant_delete_assistant |
| get-config | site-assistant_get_config |
| test-assistant | site-assistant_test_assistant |
| set-realtime | site-assistant_set_realtime |
| test-realtime | site-assistant_test_realtime |
| end-realtime-calls | site-assistant_end_realtime_calls |
| chat | site-assistant_chat |
| realtime-call | site-assistant_realtime_call |
Website