Skip to content

How to Use the Chat API

The Chat API lets you send messages to your bot and receive streaming responses programmatically. This is the same API that powers the embedded widget — you can use it to build custom integrations, mobile apps, or backend services.


  • Your bot must be set to Public privacy mode.
  • You need your bot ID (find it in the Embed tab of the Playground).

Terminal window
curl -N -X POST https://app.chatbotiq.eu/v1/public/chat/stream \
-H "Content-Type: application/json" \
-d '{
"bot_id": "YOUR-BOT-ID",
"message": "How do I get started?",
"session_id": "my-session-123"
}'

The -N flag disables curl’s output buffering so you see the streaming response in real time.


The response is a Server-Sent Events (SSE) stream. Each line is prefixed with data: followed by a JSON object. Every event has a type field that tells you how to handle it:

typePayloadWhat to do
status{"type":"status","stage":"retrieving"} then "generating"Progress hints. Safe to ignore, or show a “thinking…” indicator.
content{"type":"content","content":"some text"}A chunk of the answer. Concatenate the content fields to build the full response.
replace{"type":"replace","content":"full text"}The answer was post-filtered. Discard what you’ve shown so far and replace it with this content.
notice{"type":"notice","code":"rag_unavailable",...}A non-fatal warning (e.g. knowledge retrieval temporarily degraded). Optional to surface.
done{"type":"done","conversation_id":"...","assistant_message_id":"...","citations":[...]}End of stream. Save conversation_id to continue the conversation. citations is present only if the bot has source citations enabled.
error{"type":"error","error":"content_filtered","message":"..."}Generation failed or the response was blocked. Show message.

The answer text arrives in the content field of content events — there is no token field, and done is an event type, not a boolean flag. conversation_id and citations appear only on the final done event.

A citation object looks like:

{
"page_title": "Getting Started",
"source_url": "https://docs.example.com/start",
"source_id": "",
"score": 0.92,
"from_web_search": false
}

async function chat(botId, message, sessionId, conversationId) {
const response = await fetch('https://app.chatbotiq.eu/v1/public/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bot_id: botId,
message: message,
session_id: sessionId,
conversation_id: conversationId
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // keep any incomplete trailing line for the next read
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
switch (event.type) {
case 'content': // a chunk of the answer
fullResponse += event.content;
process.stdout.write(event.content);
break;
case 'replace': // post-filtered answer — replaces everything shown so far
fullResponse = event.content;
break;
case 'done': // end of stream
console.log('\nConversation ID:', event.conversation_id);
if (event.citations) console.log('Citations:', event.citations);
break;
case 'error':
console.error('\nError:', event.message);
break;
// 'status' and 'notice' events can be ignored
}
}
}
return fullResponse;
}

import requests
import json
def chat(bot_id, message, session_id, conversation_id=None):
response = requests.post(
'https://app.chatbotiq.eu/v1/public/chat/stream',
json={
'bot_id': bot_id,
'message': message,
'session_id': session_id,
'conversation_id': conversation_id,
},
stream=True
)
full_response = ''
for line in response.iter_lines():
if not line or not line.startswith(b'data: '):
continue
event = json.loads(line[6:])
etype = event.get('type')
if etype == 'content': # a chunk of the answer
full_response += event['content']
print(event['content'], end='', flush=True)
elif etype == 'replace': # post-filtered answer — replaces what came before
full_response = event['content']
elif etype == 'done': # end of stream
print(f"\nConversation ID: {event['conversation_id']}")
if event.get('citations'):
print(f"\nCitations: {event['citations']}")
break
elif etype == 'error':
print(f"\nError: {event.get('message')}")
break
# 'status' and 'notice' events can be ignored
return full_response

To send follow-up messages, include the conversation_id from the first response:

Terminal window
curl -N -X POST https://app.chatbotiq.eu/v1/public/chat/stream \
-H "Content-Type: application/json" \
-d '{
"bot_id": "YOUR-BOT-ID",
"message": "Tell me more about that",
"session_id": "my-session-123",
"conversation_id": "CONVERSATION-ID-FROM-FIRST-RESPONSE"
}'

There are two kinds of failures: HTTP errors returned before the stream starts, and error events delivered inside the stream once it has begun (the HTTP status is already 200 at that point).

StatusMeaningWhat to do
400Missing/invalid field, or a required reCAPTCHA token was not providedCheck the request body.
403Bot is not public, bot is inactive, request origin not in the bot’s allowed domains, free trial expired, or reCAPTCHA failedCheck the bot’s privacy mode, active status, and allowed domains.
404Bot not foundCheck the bot_id.
429Rate limit exceededWait and retry. Check the bot’s rate-limit settings.

reCAPTCHA: if the deployment is configured with reCAPTCHA, recaptcha_token is required — omitting it returns 400 and an invalid token returns 403.

Once the stream is open, failures arrive as an SSE error event rather than an HTTP status:

data: {"type":"error","error":"content_filtered","message":"..."}
data: {"type":"error","error":"service_unavailable","message":"Service temporarily unavailable"}

service_unavailable covers transient backend issues, including the workspace running out of credits (there is no 402 status on this endpoint). content_filtered means the response was blocked by the safety/grounding filters.