Kolbo.AIKolbo.AI Docs
Developer API

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) 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. 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. An identifier that is not in the catalog returns 400 with code MODEL_NOT_FOUND.

Endpoint

POST /api/v1/chat

Request Body

FieldTypeRequiredDescription
messagestringYesThe message to send. Must be a non-empty string — anything else is a 400 with code INVALID_PROMPT.
modelstringNoModel 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. Default: Smart Select.
session_idstringNoContinue 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_promptstringNoExtra instructions injected as a system message for this message only, and only when session_id is omitted. 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_promptbooleanNoDefault: true — only an explicit false (or the string "false") disables 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_searchbooleanNoForce a live web search for this message. Default: false. 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_thinkbooleanNoExtended reasoning mode. Default: false. Takes over the whole turn — see the deep-think callout. Replies take substantially longer; the MCP tool allows up to 10 minutes for a deep-think turn versus 2 minutes for a normal one.
media_urlsstring[]NoPublic URLs of images, videos, or audio to analyze. Entries must be non-empty strings; the chat pipeline keeps those starting with http or /. Video and audio trigger an automatic Gemini analysis pass before your selected model answers (see the media callout).
project_idstringNoProject 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.
  • 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_urls does 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: all three /v1/chat* routes use the default SDK generation limiter — 10 requests/minute per user — and share one bucket with every other route on that limiter: /v1/generate/video, /video/from-image, /music, /speech, /sound, /3d, /creative-director, /elements, /first-last-frame, /lipsync, /video-from-video, /v1/edit/video, and also several non-generation routes — /v1/media/upload, /v1/generate/:id/cancel, /v1/visual-dna creation, /v1/voices/clone, /v1/voices/import-elevenlabs, /v1/video/trim, /v1/moodboards reads and /v1/agents writes. Listing conversations and fetching messages count against it too, so a chatty client can rate-limit its own uploads. Image generation (30/min), media-library ops (120/min) and transcription (100/min) use separate 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, then — when you named an explicit model — a check that you hold at least that model's credit cost. Smart Select skips the second check (the model is not known yet), so an auto-routed turn can still fail after the POST succeeds.

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"}'

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, ... }] }

# 2. Use an identifier from that response
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>"
  }'

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"
  }'
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"]
  }'

Video and audio are a two-step, two-model turn. When the selected model has no native video/audio support, Kolbo first runs a Gemini analysis pass over the media and hands that analysis to your model — both models are billed on the same deduction, so credits_used is higher than the chat model's list price. Models that natively support the media type skip the extra pass. Your model is never swapped out; the analyzer only feeds it.

The skip is only evaluated for a named model. On a Smart Select turn there is no model to inspect at that point, so the analysis pass always runs and always bills — pass an explicit model with native video/audio support if you want to avoid the second leg.

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.

Media limits on an analysis turn fail 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 enforces two limits before running the analysis pass: at most 10 items total, and at most 500 MB per uploaded file. Breaching either one 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 and keep individual files under the size cap.

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, the lowest of any type; the interval table on the polling page does not list chat.

Completed Status (via polling)

{
  "success": true,
  "generation_id": "65f1c8a2e4b0a3c1d9f5e001",
  "type": "chat",
  "state": "completed",
  "progress": 100,
  "credits_used": 3,
  "credits_breakdown": [
    { "model": "<identifier>", "amount": 3, "base": 3, "final": 3, "duration_multiplier": null, "pricing": null }
  ],
  "result": {
    "content": "Quantum computing uses quantum bits (qubits)...",
    "reasoning_content": null,
    "model": "<resolved model identifier>",
    "created_at": "2026-03-10T10:30:00Z"
  }
}
FieldNotes
result.contentThe assistant reply text.
result.reasoning_contentReasoning trace when the model emits one (deep think / reasoning models). null otherwise.
result.modelThe model that actually ran — never "auto" or the router id. null when it cannot be resolved.
result.model_nameDisplay name of that model. Present only when the router recorded one.
result.image_urls | video_urls | audio_urlsPresent only when the turn produced media.
credits_used, credits_breakdownReal deducted amounts. Both keys are omitted entirely when no deduction row is linked to the turn yet (or the model was free). Each breakdown entry carries model, amount, base, final, duration_multiplier and pricing. The web-search and media-analysis legs are rolled into the chat model's single deduction, so expect one entry with an amount above the model's list price rather than one entry per leg.

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, provider — each null when the provider gave no structured detail).

List Conversations

GET /api/v1/chat/conversations

Lists 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.

ParameterTypeDescription
pagenumberPage number, 1-indexed. Default: 1.
limitnumberResults per page. Default: 20, clamped to 1–50.
project_idstringOptional — 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/messages

Returns messages oldest-first. Internal system messages are excluded. :sessionId must be a valid ObjectId (400) belonging to you (404).

ParameterTypeDescription
pagenumberPage number, 1-indexed. Default: 1.
limitnumberMessages 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.

chat_send_message submits the message and polls for you, returning the finished reply (timeouts: 120s normally, 240s with web_search, 600s with deep_think; 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 and the credit fields.

chat_get_messages trims each message down to role, content, model, 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 model when 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=chat at startup and cache them.
  • Track your own session_id per 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 overrides model, and it disables web search and media analysis for that turn — so use it only for pure reasoning prompts.
  • system_prompt is per-message, not per-session. It is only read on requests that omit session_id, and it is not stored anywhere. Once you start passing session_id, resend the persona inside message or use a custom agent.
  • Custom agents are CRUD-managed through /api/v1/agents (MCP: create_agent, list_agents, update_agent, delete_agent). POST /v1/chat has no agent parameter — agents apply to conversations started in the Kolbo app.