Skip to content

AI Messages

Messages within a session. Sending a user message triggers an AI run that generates the assistant response.

Chat Flow Walkthrough

  1. Fetch existing messages for the session (GET /ai/sessions/:sessionId/messages) to display the conversation history or pagination cursor.
  2. Send a new user message (POST /ai/sessions/:sessionId/messages) and capture both the userMessage and the assistantMessage.runId.
  3. Stream the assistant output via GET /ai/sessions/:sessionId/messages/:messageId/stream (or the run stream) and append message.delta events to the UI as they arrive.
  4. If the user interrupts the assistant, call POST /ai/sessions/:sessionId/messages/:assistantMessageId/stop to finalize the partial content and show status: stopped.
javascript
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

MethodPathDescriptionAuthRate Limit
GET/ai/sessions/:sessionId/messagesList messages (cursor/page paginated)JWT + Entitlement60/min
POST/ai/sessions/:sessionId/messagesSend a message (triggers AI run)JWT + Entitlement20/min
GET/ai/sessions/:sessionId/messages/:messageId/streamSSE message streamJWT + Entitlement20/min
POST/ai/sessions/:sessionId/messages/:assistantMessageId/stopStop generationJWT + Entitlement30/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
ParameterTypeRequiredDescription
limitnumberNoNumber of messages to return
cursorstringNoCursor for cursor-based pagination
offsetnumberNoOffset for page-based pagination
chainyIdstring (uuid)NoFilter by Chainy (AI personality). Returns 404 if the session is not associated with this Chainy.

Response

Response Example
json
{
  "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
FieldTypeDescription
idstringMessage ID
rolestringuser or assistant
contentstring | nullMessage content (null while generating)
statusstringAssistant messages: running, completed, stopped, failed
suggestionsobject[]Follow-up suggestion cards returned separately from the assistant markdown. Empty when disabled, unavailable, or not generated.
runIdstring | nullID 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.
toolCallsobject[] | nullFor Chao sessions: list of tool calls made during this message. See Tool Call Fields below.
modelIdstring | nullID of the model that generated the response
modelNamestring | nullDisplay name of the model
providerKeystring | nullProvider identifier (e.g. "anthropic", "openai")
createdAtstringISO 8601
Tool Call Fields

Each entry in toolCalls represents one tool invoked by Chao during this message:

FieldTypeDescription
idstringTool call ID (matches SSE callId, toolCallId, or stepId)
toolKeystringTool name (e.g. "bits.create", "chainies.list")
inputobjectArguments Chao passed to the tool
statusstringpending, executing, completed, failed, degraded, rejected, or skipped
outputobject | nullTool result (present when status is completed)
errorstring | nullError message (present when status is failed)
executionMsnumber | nullExecution 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 fieldPreferred lookup
toolKeypayload.toolKey ?? payload.key
callIdpayload.callId ?? payload.toolCallId ?? envelope.stepId
inputpayload.input ?? payload.args
outputpayload.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.

bash
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"
javascript
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}` } }
);
python
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
FieldTypeRequiredConstraintsDescription
contentstringYesNon-empty, ≤ 20,000 charactersMessage 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.
modelstringNoActive model keyOverride the model for this message
effortModestringNo"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.
providerstringNoLowercase identifier (e.g. openai)AI provider key. Requires a paid plan with the corresponding provider entitlement.
capabilityKeystringNoai.search.web, ai.research.deep, ai.image.generate, ai.video.generate, ai.audio.generateExplicit Chao capability to execute deterministically. When set, the matching capability-specific parameters are accepted (see Capability Parameters).
attachmentIdsstring[]NoValid file IDs (UUID v4)Array of file IDs to attach
memoryIdsstring[]NoValid memory IDs (UUID v4)Optional memory items to pin into the run
idempotencyKeystringNoClient-side dedupe key for retries
parametersobjectNoCapability-specific paramsLong-term contract for capability-specific parameters. See Capability Parameters.
structuredBlocksobject[]NoMention/media blocksStructured 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:

  1. 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"]
    }
  2. Nested parameters object (long-term contract, recommended for new code):
    json
    {
      "content": "lo-fi beat with a deep bass line",
      "capabilityKey": "ai.audio.generate",
      "parameters": {
        "durationSeconds": 20,
        "bpm": 90,
        "styleTokens": ["lo-fi", "chill"]
      }
    }
    When both forms are present, nested values win on collision.

Accepted parameters per capability:

capabilityKeyAccepted parameters
ai.audio.generatedurationSeconds (5–30), bpm (40–220), styleTokens (string[], ≤16 items, ≤64 chars each). See Audio Generation.
ai.image.generateaspectRatio (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.generateaspectRatio (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.webmaxResults (1–20)
ai.research.deepdepth (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
json
{
  "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
FieldTypeDescription
userMessage.idstringUser message ID
userMessage.rolestringAlways user
userMessage.contentstringMessage content
userMessage.createdAtstringISO 8601
assistantMessage.idstringAssistant message ID
assistantMessage.rolestringAlways assistant
assistantMessage.contentstringEmpty string while generating
assistantMessage.suggestionsobject[]Always [] in the immediate POST response shell
run.idstringAssociated AI run ID
run.statusstringInitial run status, usually queued
Error Example
json
{
  "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.

bash
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."
  }'
javascript
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();
python
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
FieldTypeDescription
eventstringSSE event type such as message.delta, message.completed, tool.started, tool.completed, run.error
data.payload.deltastringIncremental content token(s) for message.delta
data.payload.contentstringFull assistant content for message.completed
data.payload.messageIdstringAssistant 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.

bash
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"
javascript
// 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();
});
python
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":
        break

POST /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
json
{
  "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
FieldTypeDescription
idstringMessage ID
statusstringAlways stopped
contentstringPartial content generated before stopping
stoppedAtstringISO 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.

bash
curl -X POST https://api.chainabit.com/api/v1/ai/sessions/$SESSION_ID/messages/$ASSISTANT_MESSAGE_ID/stop \
  -H "Authorization: Bearer $TOKEN"
javascript
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();
python
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"]

Built with purpose.