Chat
Send messages to AI models and get responses using the Kolbo API.
Send messages to the chat models in your account's catalog — fetch the live list with GET /api/v1/models?type=chat (alias of type=text). When you omit model, Kolbo's Smart Select router picks one for the message.
Chat is asynchronous like every other Kolbo generation: POST /api/v1/chat returns immediately with a message_id and a poll_url, and you poll GET /api/v1/generate/:message_id/status for the answer. There is no token-streaming SSE endpoint on the public API.
Smart Select is the fallback, not a recommendation. Omitting model (or sending a blank string or null) routes the message through the auto-select router. The literal values "auto", "smart-select", "smart_select", "smartselect", "auto-select" and "kolbo_smart_select_router" all normalize to the same router — matching is case-insensitive and trims whitespace. On a Smart Select turn, routing_mode (fast, balanced or smart) narrows the pool the router picks from. The @kolbo/mcp tool chat_send_message deliberately steers agents toward a specific identifier instead, because auto-routing can land on a different model than the caller expects; pass an explicit model unless you actually want the router to choose.
Model identifiers are Kolbo-specific — they do not always match upstream provider names. Fetch identifiers from GET /api/v1/models?type=chat and use the identifier field. Known aliases and provider-prefixed forms of an identifier are mapped to the canonical one, but display names are not. An identifier Kolbo does not recognize is rejected with 400 MODEL_NOT_FOUND; the message suggests near matches.
Endpoint
POST /api/v1/chatRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The message to send. Must be a non-empty string — anything else is a 400 with code INVALID_PROMPT. |
model | string | No | Model identifier from GET /api/v1/models?type=chat. Must be a string — arrays/objects are a 400 INVALID_MODEL. Omit (or send an auto-select alias) for Smart Select. An unrecognized identifier returns 400 MODEL_NOT_FOUND. Default: Smart Select. |
session_id | string | No | Continue an existing conversation. Must be a valid Mongo ObjectId (400 Invalid session_id format otherwise) and must belong to you (404 Chat session not found). Omit to use today's auto session — see the callout below. |
system_prompt | string | No | Extra instructions injected as a system message for this message only, and only when session_id is omitted. They are wrapped as custom instructions that cannot override Kolbo's own system, safety or content-policy rules. It is never stored on the session, so passing it alongside session_id is silently ignored. Resend it on every request that should use it. |
enhance_prompt | boolean | No | Default: false — true (or the string "true" / "1") enables it. It does not rewrite the text your model answers: the chat message reaches the model as you sent it. What it gates is the prompt-enhancement pass for image generation / image editing started inside the chat turn, plus the standalone reference-image analysis that feeds that pass. |
web_search | boolean | No | Force a live web search for this message. Default: false. Any truthy value enables it — including the string "false", so send a real boolean. Kolbo already searches automatically when a question needs current information; true both skips that decision and upgrades the search to deep mode (more sources plus full-page reads), which costs more retrieval credits than an automatic search. Ignored when deep_think is true — see the deep-think callout. |
deep_think | boolean | No | Extended reasoning mode. Default: false. Any truthy value enables it, so send a real boolean. Takes over the whole turn — see the deep-think callout. Replies take substantially longer than a normal turn. |
thinking_level | string | No | Reasoning-effort level for models that support it. Valid ids come from the model's thinkingLevels in GET /api/v1/models?type=chat (a subset of none, minimal, low, medium, high, xhigh, max; haveThinking tells you whether the model supports it at all). Case-insensitive. Omitted or not offered by the resolved model → that model's thinkingDefault. Ignored for models without the capability. On a Smart Select turn it is applied to whichever model the router picks. Separate from deep_think, which ignores it. |
routing_mode | string | No | Smart Select profile: fast, balanced or smart (case-insensitive). Default and fallback for any other value: balanced. Only affects Smart Select turns — when no model in the catalog is tagged for the mode, the router uses its full pool. The router's catalog entry advertises these as routingModes / routingDefault. |
media_urls | string[] | No | Public URLs of images, videos, audio or documents to analyze. Entries must be non-empty strings; the chat pipeline keeps those starting with http or /. Documents (.pdf, .doc/.docx, .txt, .md, .csv, .json, .srt, .xls/.xlsx, .rtf, .html, .xml, .yaml/.yml — matched on the URL's extension) are text-extracted into the prompt. Video and audio trigger an automatic analysis pass before your selected model answers (see the media callouts). |
project_id | string | No | Project ObjectId the auto-created conversation lands in. Only honored when session_id is omitted — an existing session keeps its own project. Must be a valid ObjectId (400 SDK_PROJECT_INVALID_ID) on a project you can reach (404 SDK_PROJECT_NOT_FOUND — the same code is returned for a project that exists but is not shared with you) and where you hold owner, full or edit permission (403 SDK_PROJECT_ACCESS_DENIED). Omit to use the auto-created "API Generations" project. See Projects. |
deep_think: true takes over the turn — it ignores model. Deep think does not layer reasoning onto your selected model. It routes the whole turn to Kolbo's fixed reasoning model regardless of what you sent in model, and that is the model the status response reports and the one the deduction is attributed to. Three further consequences on a deep-think turn:
- Smart Select never runs — the router is skipped entirely, and neither
routing_modenorthinking_levelhas any effect. - Web search never runs, even with
web_search: true. The augmentation step is not reachable from the deep-think path, so no sources are retrieved and no citations appear. - The video/audio analysis pass never runs. Media you pass in
media_urlsdoes not get the Gemini pre-analysis described below, so a deep-think turn answers as if the video or audio was not there.
Send deep_think: false (or omit it) whenever you need your own model, live sources, or media analysis.
Omitting session_id does not always start a fresh conversation. The API reuses one auto session per (user, project, day), named API Chat - YYYY-MM-DD. Two messages sent the same day without a session_id land in the same conversation and share context. To guarantee an isolated thread, keep your own session_id per logical conversation.
Rate limit: POST /v1/chat uses the SDK generation limiter — 30 requests/minute per user — and shares one bucket with every other route on that limiter: /v1/generate/video, /video/from-image, /music (plus its reference-audio, edit-section, import, extend and cover routes), /speech, /sound, /3d, /creative-director, /elements, /first-last-frame, /lipsync, /video-from-video, /v1/edit/video, /v1/analyze/video, and also several non-generation routes — /v1/media/upload, /v1/generate/:id/cancel, /v1/visual-dna create/update and character sheets, /v1/voices/clone, /v1/voices/import-elevenlabs, /v1/video/trim, /v1/lipsync-plans writes and /v1/skills (alias /v1/agents) writes. Requests rejected with an error status do not consume the budget. The two read routes — listing conversations and fetching messages — sit on the separate media-library limiter (120/min). Image generation (30/min) and transcription (100/min) also have their own buckets, and GET /v1/generate/:id/status has no per-route limiter, so polling never eats your generation budget. A global safety net still sits above every route, polling included: 5,000 requests/minute per API key (keyed on the key itself, so each key gets its own bucket rather than sharing your office IP's).
Request bodies are capped at 1 MB. A larger JSON payload is rejected by the body parser before the handler runs. Pass long documents as media_urls instead of pasting them into message.
Insufficient credits fail synchronously. Two gates run before the message is queued, both returning 403 with code INSUFFICIENT_CREDITS on the initial POST rather than during polling: a blanket check that your balance is at least 1 credit (its body also carries balance, required and pricing_url), then — when you named an explicit model — a check that you hold at least that model's credit cost (its body is just success, error and code). Smart Select skips the second check (the model is not known yet), so an auto-routed turn can still fail after the POST succeeds.
Errors
| Status | Code | When |
|---|---|---|
400 | INVALID_PROMPT | message missing, empty or not a string |
400 | INVALID_MODEL | model is not a string |
400 | — | session_id is not a valid ObjectId (Invalid session_id format) |
400 / 403 / 404 | SDK_PROJECT_INVALID_ID / SDK_PROJECT_ACCESS_DENIED / SDK_PROJECT_NOT_FOUND | project_id problems (only checked when session_id is omitted) |
403 | INSUFFICIENT_CREDITS | Either credit gate above |
404 | — | session_id is not one of your live chat sessions (Chat session not found) |
429 | — | Rate limit exceeded |
Any other failure raised by the chat pipeline before the message is queued is returned as { "success": false, "error": "...", "code": "..." } with the pipeline's status code; code falls back to CHAT_ERROR when the pipeline gives none.
API chat opts out of user memory. Neither your personal memories nor the auto-generated project-context summaries are injected into the prompt. Every message sent through POST /v1/chat also stamps its session as already-mined, so the auto-memory and project-context workers skip it — note this applies to any session you post into, including one you originally started in the Kolbo web app. The stamp is not permanent: the next message sent to that session from the web app clears both markers and mining resumes.
They are not fully isolated from the project, though: the conversation still lives in a project, and that project's linked Visual DNAs / moodboards (the "cast" directive) and its file knowledge base (vector search over project files) are still injected on every turn. Send project_id pointing at an empty project if you want a clean-room conversation.
Examples
Basic (Smart Select)
# No "model" field — Smart Select picks the model
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"message": "Explain quantum computing in simple terms"}'
# Same, but ask the router to favor its faster profile
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"message": "Explain quantum computing in simple terms", "routing_mode": "fast"}'With a Specific Model
Fetch the available models first and use an identifier from the response:
# 1. List chat models to get valid identifiers
curl "https://api.kolbo.ai/api/v1/models?type=chat" \
-H "X-API-Key: kolbo_live_..."
# Response: { "models": [{ "identifier": "...", "name": "...", "credit": 1,
# "haveThinking": true, "thinkingLevels": [{ "id": "low", "label": "Low" }, ...],
# "thinkingDefault": "...", ... }] }
# 2. Use an identifier from that response (thinking_level only if the model lists it)
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Write a Python function to sort a list",
"model": "<identifier from step 1>",
"thinking_level": "low"
}'Multi-Turn Conversation
# First message
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"message": "What is machine learning?"}'
# Response includes session_id
# {"success": true, "session_id": "65f1c8a2e4b0a3c1d9f5e789", ...}
# Follow-up message in the same conversation
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Give me a practical example",
"session_id": "65f1c8a2e4b0a3c1d9f5e789"
}'With Web Search
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "What are the latest AI developments this week?",
"web_search": true
}'Web search does not swap your model. Kolbo retrieves sources, injects them into the prompt, and your selected model answers with inline [n] citation markers. The retrieval cost is billed on the same deduction as the model reply and shows up in credits_used when you poll.
Citation sources are not returned by the API. The answer text carries the [n] markers, but the URL list behind them lives in message metadata that neither GET /v1/generate/:id/status nor GET /v1/chat/conversations/:sessionId/messages returns. Ask the model to spell out the URLs in its answer if you need them.
Image / Video / Audio Analysis
Pass public media URLs in media_urls. Routing is automatic — you do not need to name a vision model:
# Analyze an image
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "What is shown in this image? Describe all the text and visual elements.",
"media_urls": ["https://cdn.kolbo.ai/your-image.jpg"]
}'
# Analyze a video
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Describe what happens in this video and read any text shown on screen.",
"media_urls": ["https://cdn.kolbo.ai/your-video.mp4"]
}'
# Read a document (text is extracted into the prompt)
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Summarize the key terms in this contract.",
"media_urls": ["https://cdn.kolbo.ai/your-contract.pdf"]
}'Video and audio are a two-step, two-model turn. Whenever the turn carries a video, an audio file or a YouTube link, Kolbo first runs a dedicated Gemini analysis pass over the media and hands that analysis to your model — and both models are billed, so credits_used is higher than the chat model's list price. This happens even when your model natively accepts video or audio: there is no model you can pick to skip the second leg. Your model is never swapped out; the analyzer only feeds it. Any images on the same turn are analyzed in that pass too.
Images behave differently from video/audio. An image-only turn never triggers the two-step path. If your model has native vision the images are passed to it directly; if it is text-only, Kolbo pre-analyzes the images and injects the description as text so your model still answers. That image-analysis pass is not billed — only video/audio analysis adds credits.
The media count limit on an analysis turn fails silently. Once any video, audio file or YouTube link is present, Kolbo counts every image, video, audio file and YouTube link on the turn and allows at most 10 items. Breaching it aborts the whole analysis pass — no error is returned, your model still answers, and the reply reads as if the media was not there. Split large batches across messages. (The pipeline's 500 MB per-file check applies only to direct file uploads, which this endpoint does not accept; URLs in media_urls are not size-checked at this step.)
A YouTube link in message counts as media. URLs are extracted from the message text itself, so a bare YouTube link triggers the same Gemini analysis pass (and its billing) even when media_urls is empty.
For local files, upload first via POST /api/v1/media/upload to get a stable CDN URL, then pass that URL in media_urls.
With Deep Thinking
curl -X POST https://api.kolbo.ai/api/v1/chat \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Solve this math problem step by step: ...",
"deep_think": true
}'JavaScript
const API_KEY = "kolbo_live_..."; // Replace with your API key
const BASE = "https://api.kolbo.ai/api";
// Smart Select: omit "model" to auto-pick the model
async function chat(message, sessionId) {
const body = { message };
if (sessionId) body.session_id = sessionId;
const res = await fetch(`${BASE}/v1/chat`, {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
// poll_url is returned WITHOUT the /api prefix — prepend BASE
let status;
do {
await new Promise((r) => setTimeout(r, (data.poll_interval_hint || 2) * 1000));
const poll = await fetch(`${BASE}${data.poll_url}`, {
headers: { "X-API-Key": API_KEY },
});
status = await poll.json();
} while (status.state === "processing");
if (status.state === "failed") throw new Error(status.error);
// `cancelled` carries neither `result` nor `error` — reading result.content would throw.
if (status.state === "cancelled") throw new Error("Chat turn was cancelled");
return { content: status.result.content, session_id: data.session_id };
}
// Usage
async function main() {
const first = await chat("Explain the theory of relativity");
console.log(first.content);
// Continue the conversation
const followUp = await chat("Can you simplify that?", first.session_id);
console.log(followUp.content);
}
main();Python
import requests, time
API_KEY = "kolbo_live_..." # Replace with your API key
BASE = "https://api.kolbo.ai/api"
HEADERS = {"X-API-Key": API_KEY}
def chat(message, session_id=None):
"""Send a chat message. Omits 'model' so Smart Select picks one."""
body = {"message": message}
if session_id:
body["session_id"] = session_id
data = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json=body).json()
assert data["success"], data.get("error", "Request failed")
# poll_url is returned WITHOUT the /api prefix — prepend BASE
while True:
time.sleep(data.get("poll_interval_hint", 2))
status = requests.get(f"{BASE}{data['poll_url']}", headers=HEADERS).json()
if status["state"] != "processing":
break
# `cancelled` carries neither `result` nor `error`, so assert on `completed`
# rather than only checking for `failed`.
assert status["state"] == "completed", status.get("error", status["state"])
return {"content": status["result"]["content"], "session_id": data["session_id"]}
# Usage
first = chat("Explain the theory of relativity")
print(first["content"])
# Continue the conversation
follow_up = chat("Can you simplify that?", first["session_id"])
print(follow_up["content"])Response
Message Accepted
{
"success": true,
"message_id": "65f1c8a2e4b0a3c1d9f5e001",
"session_id": "65f1c8a2e4b0a3c1d9f5e789",
"type": "chat",
"model": "auto",
"poll_url": "/v1/generate/65f1c8a2e4b0a3c1d9f5e001/status",
"poll_interval_hint": 2
}model echoes "auto" when Smart Select is in play; the model that actually ran is reported by the status endpoint. poll_url is relative to https://api.kolbo.ai/api — prepend that base.
Chat is the one endpoint that does not return generation_id. The job handle is message_id. Every other generation endpoint returns generation_id, so a shared poll helper written as pollUntilDone(started.generation_id, …) — including the reference loop in Polling & Cancellation — receives undefined here and polls /v1/generate/undefined/status, which answers 400 Invalid generation ID format. Pass started.message_id (or just use started.poll_url).
Note also that the status response for that same job reports the id back under generation_id, not message_id — the two names refer to the same value. And poll_interval_hint is 2 seconds on chat.
Completed Status (via polling)
{
"success": true,
"generation_id": "65f1c8a2e4b0a3c1d9f5e001",
"type": "chat",
"state": "completed",
"progress": 100,
"session_id": "65f1c8a2e4b0a3c1d9f5e789",
"project_id": "65f1c8a2e4b0a3c1d9f5e123",
"credits_used": 3,
"credits_breakdown": [
{ "model": "<identifier>", "amount": 3 }
],
"result": {
"content": "Quantum computing uses quantum bits (qubits)...",
"reasoning_content": null,
"model": "<resolved model identifier>",
"created_at": "2026-03-10T10:30:00Z"
}
}| Field | Notes |
|---|---|
result.content | The assistant reply text. |
result.reasoning_content | Reasoning trace when the model emits one (deep think / reasoning models). null otherwise. |
result.model | The model that actually ran — never "auto" or the router id. null when it cannot be resolved. |
result.model_name | Display name of that model. Present only when the router recorded one. |
result.image_urls | video_urls | audio_urls | Present only when the turn produced media. |
result.created_at | When the assistant message was created. |
session_id, project_id | The conversation and project the turn landed in. |
credits_used, credits_breakdown | Real deducted amounts. Both keys are omitted entirely when no deduction row is linked to the turn yet (or the model was free). credits_used is the sum; credits_breakdown has one { model, amount } entry per deduction row linked to the turn. Web-search and media-analysis costs are included, so the total can sit above the model's list price — read credits_used rather than estimating from the catalog. |
Poll until state leaves processing. The status endpoint deliberately holds a chat turn at processing until text or media actually lands, because the message is flagged complete before the streamed tokens are persisted — a naive "stop at first completed" poller reads an empty reply. A genuinely empty completion is released after 90 seconds. Failed turns return state: "failed" with error plus a failure object (message, category, code, retryable, severity — each null when no structured detail was recorded). An unknown or foreign id returns 404 Generation not found; a malformed one 400 Invalid generation ID format.
List Conversations
GET /api/v1/chat/conversationsLists all of your chat conversations — including ones created in the Kolbo web app, not just API-created ones — across all projects, most recent activity first.
| Parameter | Type | Description |
|---|---|---|
page | number | Page number, 1-indexed. Default: 1. |
limit | number | Results per page. Default: 20, clamped to 1–50. |
project_id | string | Optional — restrict to one project. Must be a valid ObjectId (400 Invalid project_id. otherwise). Alias: projectId. See Projects. |
curl "https://api.kolbo.ai/api/v1/chat/conversations" \
-H "X-API-Key: kolbo_live_..."{
"success": true,
"conversations": [
{
"session_id": "65f1c8a2e4b0a3c1d9f5e789",
"name": "API Chat - 2026-03-10",
"project_id": "65f1c8a2e4b0a3c1d9f5e123",
"last_activity": "2026-03-10T10:30:00Z",
"created_at": "2026-03-10T09:00:00Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
}Get Conversation Messages
GET /api/v1/chat/conversations/:sessionId/messagesReturns messages oldest-first. Internal system messages are excluded. :sessionId must be a valid ObjectId (400) belonging to you (404).
| Parameter | Type | Description |
|---|---|---|
page | number | Page number, 1-indexed. Default: 1. |
limit | number | Messages per page. Default: 50, clamped to 1–100. |
curl "https://api.kolbo.ai/api/v1/chat/conversations/65f1c8a2e4b0a3c1d9f5e789/messages" \
-H "X-API-Key: kolbo_live_..."{
"success": true,
"session_id": "65f1c8a2e4b0a3c1d9f5e789",
"messages": [
{
"message_id": "65f1c8a2e4b0a3c1d9f5e000",
"role": "user",
"content": "Explain quantum computing",
"status": "completed",
"created_at": "2026-03-10T10:30:00Z"
},
{
"message_id": "65f1c8a2e4b0a3c1d9f5e001",
"role": "assistant",
"content": "Quantum computing uses quantum bits...",
"status": "completed",
"model": {
"identifier": "<identifier>",
"name": "<display name>",
"provider": "<provider>"
},
"credits_used": 3,
"created_at": "2026-03-10T10:30:05Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 2, "pages": 1 }
}Optional per-message fields, emitted only when present: reasoning_content, image_urls, video_urls, audio_urls (all CDN-rewritten), and credits_used.
Publishing HTML from a Chat
There is no /api/v1 route for publishing an artifact. The MCP tool publish_html_artifact calls POST /api/artifact/quick-share — an app route that also accepts X-API-Key. See HTML Artifacts.
MCP Tools
The matching @kolbo/mcp tools are chat_send_message, chat_list_conversations, and chat_get_messages — one per endpoint on this page, with the same argument names (including thinking_level and routing_mode). Two client-side differences on chat_send_message: its model argument also accepts a catalog display name, which the tool resolves to an identifier before calling the API, and its enhance_prompt is sent as false unless you pass true.
chat_send_message submits the message and polls for you, returning the finished reply. It waits at most 150 seconds for every turn, deep think and web search included; on timeout it returns { state: "processing", generation_id, _timed_out: true } rather than an error — the turn is still running and nothing was refunded, so keep polling GET /v1/generate/:id/status with that id. Its result carries session_id, message_id, model, content, reasoning_content, image_urls, video_urls, audio_urls and the credit fields.
chat_get_messages trims each message down to role, content, model (the display name, falling back to the identifier), status and created_at to keep agent context small — message_id, reasoning_content and credits_used are dropped. It also emits image_url, video_url and audio_url keys, but they are always null: the tool reads singular field names while the endpoint returns the plural image_urls / video_urls / audio_urls. Call GET /v1/chat/conversations/:sessionId/messages directly when you need media URLs or the full record.
Tips
- Pass an explicit
modelwhen the answer's shape matters. Omitting it hands the choice to Smart Select, which can route two identical prompts to different models. - Don't hardcode model names. Fetch identifiers from
GET /api/v1/models?type=chatat startup and cache them. - Track your own
session_idper conversation — relying on the omit-to-start-new behavior silently merges same-day threads. - Deep thinking (
deep_think: true) suits math, logic, and analysis. It takes much longer, it overridesmodel, and it disables web search and media analysis for that turn — so use it only for pure reasoning prompts. system_promptis per-message, not per-session. It is only read on requests that omitsession_id, and it is not stored anywhere. Once you start passingsession_id, resend the persona insidemessage—POST /v1/chathas no skill/agent parameter.- Skills (formerly custom agents) are CRUD-managed through
/api/v1/skills, with/api/v1/agentskept as an alias (MCP:create_skill,list_skills,update_skill,delete_skill, or the original*_agentnames).POST /v1/chathas no skill parameter — skills apply to conversations started in the Kolbo app.