AI Messages
Messages within a session. Sending a user message triggers an AI run that generates the assistant response.
Chat Flow Walkthrough
- Fetch existing messages for the session (
GET /ai/sessions/:sessionId/messages) to display the conversation history or pagination cursor. - Send a new user message (
POST /ai/sessions/:sessionId/messages) and capture both theuserMessageand theassistantMessage.runId. - Stream the assistant output via
GET /ai/sessions/:sessionId/messages/:messageId/stream(or the run stream) and appendmessage.deltaevents to the UI as they arrive. - If the user interrupts the assistant, call
POST /ai/sessions/:sessionId/messages/:assistantMessageId/stopto finalize the partial content and showstatus: stopped.
const startChat = async (sessionId) => {
const history = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
console.log("Existing messages:", (await history.json()).data);
const reply = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "Show me a recap of today's tasks." }),
}
);
const {
assistantMessage: { id: assistantId },
} = (await reply.json()).data;
const stream = new EventSource(
`${BASE_URL}/ai/sessions/${sessionId}/messages/${assistantId}/stream`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
stream.addEventListener("message.delta", (event) => {
const { payload } = JSON.parse(event.data);
console.log("Delta:", payload.delta);
});
};Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /ai/sessions/:sessionId/messages | List messages (cursor/page paginated) | JWT + Entitlement | 60/min |
| POST | /ai/sessions/:sessionId/messages | Send a message (triggers AI run) | JWT + Entitlement | 20/min |
| GET | /ai/sessions/:sessionId/messages/:messageId/stream | SSE message stream | JWT + Entitlement | 20/min |
| POST | /ai/sessions/:sessionId/messages/:assistantMessageId/stop | Stop generation | JWT + Entitlement | 30/min |
GET /ai/sessions/:sessionId/messages
Description
List messages in a session with cursor-based or page-based pagination.
Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 60/min
Request
- Headers:
Authorization: Bearer <token> - Path params:
sessionId
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Number of messages to return |
cursor | string | No | Cursor for cursor-based pagination |
offset | number | No | Offset for page-based pagination |
chainyId | string (uuid) | No | Filter by Chainy (AI personality). Returns 404 if the session is not associated with this Chainy. |
Response
Response Example
{
"data": [
{
"id": "cm5msg001",
"role": "user",
"content": "How can I improve my vocabulary retention rate?",
"createdAt": "2026-03-17T10:05:00.000Z"
},
{
"id": "cm5msg002",
"role": "assistant",
"content": "Spaced repetition is one of the most effective techniques...",
"status": "completed",
"toolCalls": [
{
"id": "tc_abc123",
"toolKey": "memory.search",
"input": { "query": "vocabulary retention" },
"status": "completed",
"output": { "total": 2 },
"error": null,
"executionMs": 64
}
],
"suggestions": [
{
"suggestion": "Can you turn that into a 7-day vocabulary routine?",
"priority": 1
},
{
"suggestion": "What should I do when I keep forgetting the same words?",
"priority": 2
}
],
"tokensUsed": 850,
"createdAt": "2026-03-17T10:05:00.000Z"
}
],
"meta": {
"cursor": "eyJpZCI6ImNtNW1zZzAwMiJ9",
"hasMore": false,
"total": 2
}
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Message ID |
role | string | user or assistant |
content | string | null | Message content (null while generating) |
status | string | Assistant messages: running, completed, stopped, failed |
suggestions | object[] | Follow-up suggestion cards returned separately from the assistant markdown. Empty when disabled, unavailable, or not generated. |
runId | string | null | ID of the AI run that produced this message. Use this to attach a stream listener on page reload. Present on all assistant messages once the run is created. |
toolCalls | object[] | null | For Chao sessions: list of tool calls made during this message. See Tool Call Fields below. |
modelId | string | null | ID of the model that generated the response |
modelName | string | null | Display name of the model |
providerKey | string | null | Provider identifier (e.g. "anthropic", "openai") |
createdAt | string | ISO 8601 |
Tool Call Fields
Each entry in toolCalls represents one tool invoked by Chao during this message:
| Field | Type | Description |
|---|---|---|
id | string | Tool call ID (matches SSE callId, toolCallId, or stepId) |
toolKey | string | Tool name (e.g. "bits.create", "chainies.list") |
input | object | Arguments Chao passed to the tool |
status | string | pending, executing, completed, failed, degraded, rejected, or skipped |
output | object | null | Tool result (present when status is completed) |
error | string | null | Error message (present when status is failed) |
executionMs | number | null | Execution duration in milliseconds |
toolCalls is persisted and returned on every history request. On page reload, reconstruct tool cards from this field and re-attach a stream listener when the assistant message still has an active runId.
Normalize live SSE payloads into the same card shape:
| Normalized field | Preferred lookup |
|---|---|
toolKey | payload.toolKey ?? payload.key |
callId | payload.callId ?? payload.toolCallId ?? envelope.stepId |
input | payload.input ?? payload.args |
output | payload.data ?? payload.output |
When payload.activity exists, render card title, detail, status, and subject from that object.
Code Examples
Use the id from Create a Session's response (data.id) as $SESSION_ID. For the Chainy filter, use the id from Create Chainy as $CHAINY_ID.
curl "https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages?limit=20" \
-H "Authorization: Bearer $TOKEN"
# Filter by Chainy:
curl "https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages?limit=20&chainyId=$CHAINY_ID" \
-H "Authorization: Bearer $TOKEN"const sessionId = process.env.SESSION_ID; // id from Create a Session's response
const params = new URLSearchParams({ limit: "20" });
const res = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages?${params}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { data, meta } = await res.json();
// Filter by Chainy:
const chainyId = process.env.CHAINY_ID; // id of an existing Chainy (see Create Chainy)
const chainyParams = new URLSearchParams({
limit: "20",
chainyId,
});
const chainyRes = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages?${chainyParams}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);import os
import requests
session_id = os.environ["SESSION_ID"] # id from Create a Session's response
res = requests.get(
f"{BASE_URL}/ai/sessions/{session_id}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
params={"limit": 20},
)
body = res.json()
# Filter by Chainy:
chainy_id = os.environ["CHAINY_ID"] # id of an existing Chainy (see Create Chainy)
chainy_res = requests.get(
f"{BASE_URL}/ai/sessions/{session_id}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
params={
"limit": 20,
"chainyId": chainy_id,
},
)POST /ai/sessions/:sessionId/messages
Description
Send a user message to a session, which triggers an AI run to generate the assistant response.
Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 20/min
Request
- Headers:
Authorization: Bearer <token>,Content-Type: application/json - Path params:
sessionId
Request Body
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
content | string | Yes | Non-empty, ≤ 20,000 characters | Message content. The 20,000-character cap applies to every caller, including the seed message returned by feature endpoints such as POST /productivity/priority-analysis/runs. |
model | string | No | Active model key | Override the model for this message |
effortMode | string | No | "basic", "thinking", or "pro" | High-level model tier preference. basic uses a standard model, thinking selects a thinking-capable model, pro selects the highest-capability tier. Overrides the session-level effortMode if both are set. Exact model selection remains a backend hint. |
provider | string | No | Lowercase identifier (e.g. openai) | AI provider key. Requires a paid plan with the corresponding provider entitlement. |
capabilityKey | string | No | ai.search.web, ai.research.deep, ai.image.generate, ai.video.generate, ai.audio.generate | Explicit Chao capability to execute deterministically. When set, the matching capability-specific parameters are accepted (see Capability Parameters). |
attachmentIds | string[] | No | Valid file IDs (UUID v4) | Array of file IDs to attach |
memoryIds | string[] | No | Valid memory IDs (UUID v4) | Optional memory items to pin into the run |
idempotencyKey | string | No | — | Client-side dedupe key for retries |
parameters | object | No | Capability-specific params | Long-term contract for capability-specific parameters. See Capability Parameters. |
structuredBlocks | object[] | No | Mention/media blocks | Structured user-side blocks such as agent mentions or media references |
Capability-specific fields (such as durationSeconds, aspectRatio, generateAudio, etc.) may also be sent at the top level for backward compatibility. See the section below.
Capability Parameters
When capabilityKey is set, the message endpoint accepts the parameters that capability declares. Two equivalent request shapes are supported:
- Top-level fields (current client compatibility):json
{ "content": "lo-fi beat with a deep bass line", "capabilityKey": "ai.audio.generate", "durationSeconds": 20, "bpm": 90, "styleTokens": ["lo-fi", "chill"] } - Nested
parametersobject (long-term contract, recommended for new code):jsonWhen both forms are present, nested values win on collision.{ "content": "lo-fi beat with a deep bass line", "capabilityKey": "ai.audio.generate", "parameters": { "durationSeconds": 20, "bpm": 90, "styleTokens": ["lo-fi", "chill"] } }
Accepted parameters per capability:
| capabilityKey | Accepted parameters |
|---|---|
ai.audio.generate | durationSeconds (5–30), bpm (40–220), styleTokens (string[], ≤16 items, ≤64 chars each). See Audio Generation. |
ai.image.generate | aspectRatio (W:H), quality (standard|hd|auto), numImages / numberOfImages (alias for count, 1–4), seed (int), negativePrompt (≤4000 chars), size (1024x1024|1792x1024|1024x1792), style (vivid|natural), resolution, provider, model, activeImageId. See Generative Media. |
ai.video.generate | aspectRatio (16:9|9:16|1:1|4:3|3:4|21:9), duration (1–60, accepts numeric strings), resolution (480p|720p|1080p|4k), size (WxH), generateAudio (boolean), negativePrompt, seed (int), styleTokens, inputImageUrl, inputVideoUrl, referenceImages[], referenceVideos[], provider (kling|veo|sora), model. See Video Generation. |
ai.search.web | maxResults (1–20) |
ai.research.deep | depth (standard|deep) |
Capability fields belonging to a different capability than the one selected are rejected with a precise error (bpm is only valid for ai.audio.generate). Truly unknown fields are also rejected with unknown property X. Plain chat (no capabilityKey) keeps the original DTO contract — extra fields are still rejected.
Response
Response Example
{
"data": {
"sessionId": "$SESSION_ID",
"userMessage": {
"id": "cm5msg001",
"role": "user",
"content": "How can I improve my vocabulary retention rate? I keep forgetting words after a few days.",
"createdAt": "2026-03-17T10:05:00.000Z"
},
"assistantMessage": {
"id": "cm5msg002",
"role": "assistant",
"content": "",
"suggestions": [],
"createdAt": "2026-03-17T10:05:00.000Z"
},
"run": {
"id": "cm5run003",
"status": "queued"
}
}
}The assistant message shell is returned immediately with suggestions: []. Suggestions are produced by the same model call that writes the assistant reply and are delivered via history reads and the message.suggestions SSE event in the same tick as message.completed.
Response Fields
| Field | Type | Description |
|---|---|---|
userMessage.id | string | User message ID |
userMessage.role | string | Always user |
userMessage.content | string | Message content |
userMessage.createdAt | string | ISO 8601 |
assistantMessage.id | string | Assistant message ID |
assistantMessage.role | string | Always assistant |
assistantMessage.content | string | Empty string while generating |
assistantMessage.suggestions | object[] | Always [] in the immediate POST response shell |
run.id | string | Associated AI run ID |
run.status | string | Initial run status, usually queued |
Error Example
{
"error": {
"code": "bad_request",
"message": "bpm is only valid for ai.audio.generate; unknown property foo",
"details": {
"fields": [
{ "field": "bpm", "message": "bpm is only valid for ai.audio.generate", "capabilityKey": "ai.audio.generate" },
{ "field": "foo", "message": "unknown property foo" }
]
}
},
"meta": { "requestId": "<request-id>" }
}Code Examples
Use the id from Create a Session's response (data.id) as $SESSION_ID.
curl -X POST https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "How can I improve my vocabulary retention rate? I keep forgetting words after a few days."
}'const sessionId = process.env.SESSION_ID; // id from Create a Session's response
const res = await fetch(`${BASE_URL}/ai/sessions/${sessionId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
content:
"How can I improve my vocabulary retention rate? I keep forgetting words after a few days.",
}),
});
const { data } = await res.json();import os
import requests
session_id = os.environ["SESSION_ID"] # id from Create a Session's response
res = requests.post(
f"{BASE_URL}/ai/sessions/{session_id}/messages",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
},
json={
"content": "How can I improve my vocabulary retention rate? I keep forgetting words after a few days."
},
)
data = res.json()["data"]GET /ai/sessions/:sessionId/messages/:messageId/stream
Description
Connect to a Server-Sent Events stream to receive incremental content as an assistant message is generated.
Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 20/min
Request
- Headers:
Authorization: Bearer <token>,Accept: text/event-stream - Path params:
sessionId,messageId
Response
Response Example
event: message.delta
data: {"eventId":1,"type":"message.delta","runId":"cm5run003","payload":{"delta":"Spaced repetition is one of the most "}}
event: message.delta
data: {"eventId":2,"type":"message.delta","runId":"cm5run003","payload":{"delta":"effective techniques for long-term retention. "}}
event: message.delta
data: {"eventId":3,"type":"message.delta","runId":"cm5run003","payload":{"delta":"Here are some strategies tailored to your chain..."}}
event: message.completed
data: {"eventId":4,"type":"message.completed","runId":"cm5run003","payload":{"messageId":"cm5msg002","content":"Spaced repetition is one of the most effective techniques for long-term retention. Here are some strategies tailored to your chain..."}}Response Fields
| Field | Type | Description |
|---|---|---|
event | string | SSE event type such as message.delta, message.completed, tool.started, tool.completed, run.error |
data.payload.delta | string | Incremental content token(s) for message.delta |
data.payload.content | string | Full assistant content for message.completed |
data.payload.messageId | string | Assistant message ID when present |
Code Examples
Use $SESSION_ID from Create a Session's response (data.id), and use the id from Send a Message's response (data.assistantMessage.id) as $MESSAGE_ID.
curl -N "https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages/$MESSAGE_ID/stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"// Browser — uses built-in EventSource (no auth header support)
// For Node.js, install: npm install eventsource
import EventSource from 'eventsource';
const sessionId = process.env.SESSION_ID; // id from Create a Session's response
const messageId = process.env.MESSAGE_ID; // assistantMessage.id from Send a Message's response
const es = new EventSource(
`${BASE_URL}/ai/sessions/${sessionId}/messages/${messageId}/stream`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
es.addEventListener('message.delta', (e) => {
const { payload } = JSON.parse(e.data);
process.stdout.write(payload.delta);
});
es.addEventListener('message.completed', () => es.close());
es.addEventListener('run.error', (e) => {
const { payload } = JSON.parse(e.data);
console.error('Run error:', payload.error);
es.close();
});import os
import sseclient
import requests
session_id = os.environ["SESSION_ID"] # id from Create a Session's response
message_id = os.environ["MESSAGE_ID"] # assistantMessage.id from Send a Message's response
response = requests.get(
f"{BASE_URL}/ai/sessions/{session_id}/messages/{message_id}/stream",
headers={
"Authorization": f"Bearer {TOKEN}",
"Accept": "text/event-stream",
},
stream=True,
)
client = sseclient.SSEClient(response)
for event in client.events():
if event.event == "message.delta":
print(event.data, end="", flush=True)
elif event.event == "message.completed":
breakPOST /ai/sessions/:sessionId/messages/:assistantMessageId/stop
Description
Stop an in-progress assistant message generation.
Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 30/min
Request
- Headers:
Authorization: Bearer <token> - Path params:
sessionId,assistantMessageId
Response
Response Example
{
"data": {
"id": "cm5msg002",
"status": "stopped",
"content": "Spaced repetition is one of the most effective techniques for long-term retention. ",
"stoppedAt": "2026-03-17T10:05:05.000Z"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Message ID |
status | string | Always stopped |
content | string | Partial content generated before stopping |
stoppedAt | string | ISO 8601 timestamp of stop |
Code Examples
Use $SESSION_ID from Create a Session's response (data.id), and use the id from Send a Message's response (data.assistantMessage.id) as $ASSISTANT_MESSAGE_ID.
curl -X POST https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages/$ASSISTANT_MESSAGE_ID/stop \
-H "Authorization: Bearer $TOKEN"const sessionId = process.env.SESSION_ID; // id from Create a Session's response
const assistantMessageId = process.env.ASSISTANT_MESSAGE_ID; // assistantMessage.id from Send a Message's response
const res = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages/${assistantMessageId}/stop`,
{
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}` },
}
);
const { data } = await res.json();import os
import requests
session_id = os.environ["SESSION_ID"] # id from Create a Session's response
assistant_message_id = os.environ["ASSISTANT_MESSAGE_ID"] # assistantMessage.id from Send a Message's response
res = requests.post(
f"{BASE_URL}/ai/sessions/{session_id}/messages/{assistant_message_id}/stop",
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]