---
title: "Send a chat message"
method: POST
path: "/api/chat"
tags: ["AI"]
---

# Send a chat message

`POST /api/chat`

## Request

- Base URL: `https://your-deployment.example.com`
- URL: `https://your-deployment.example.com/api/chat`
- Auth: one of:
  - HTTP bearer
  - API key in header `x-api-key`

Sends a message to the AI and creates a quest to process it. By default (async) the call returns immediately with a quest id; poll `GET /api/v1/quests/{id}` for the reply. Send `wait: true` to block until the reply is ready and receive it inline. A `wait: true` turn is bounded at about 45 seconds: one that runs longer is ended (unused credits refunded) and answered with a 504 naming the quest in `quest_id`. Send long or tool-heavy turns (large attachments, tool calls) in the default async mode and poll instead. A tool that produced machine-readable state reports it under `toolPayloads` - an array of `{ type, payload }` entries in emission order, alongside (never instead of) the prose reply - on the `wait: true` body and on the polled quest. A turn can FAIL after the ACK - reported on the polled quest as `type: "error"`, since the ACK was already sent. A terminal `status: "stopped"` (a missing session, a user-cancelled turn) is ALSO a failure even without `type: "error"` - it carries an explanatory string in `reply`/`replies` rather than an answer. `type` is the failure signal for the classified failure classes (an abort, a provider timeout, credit exhaustion); `errorCode` is an optional refinement present only when the failure is a classified billing reason. A recovered stuck quest is NOT in that list: one that still has renderable content resolves as a success by design. `QUEST_ERROR_CODES` has two members, but only `insufficient_credits` is raised as a quest errorCode by any current throw site on this endpoint - `spend_cap_exceeded` is thrown only by the embed chat route's pre-flight, which fires outside the process try/catch that would classify it onto a quest. A caller must treat `type: "error"` OR a terminal `status: "stopped"` as failure even when `errorCode` is absent, and must not read `reply` as an answer without checking those first. Authenticate with an API key (`b4m_live_`) or a JWT. Session resolution: pass `sessionId` to continue an existing notebook. An API-key caller that omits it gets a BRAND-NEW notebook rather than the notebook the human last had open, and `newConversation: true` forces a new notebook for any auth mode; both return the new id as `sessionId`, so the caller can continue the conversation by passing it back. A first-party JWT caller that omits `sessionId` keeps the last-opened-notebook fallback. Sending `sessionId` and `newConversation` together is a 422.

## Request body

- SendChatMessageRequest
  - `sessionId` string, nullable
  - `newConversation` boolean
  - `message` string, required
  - `organizationId` string
  - `model` string
  - `temperature` number
  - `max_tokens` number
  - `maxTokens` number
  - `maxOutputTokens` number
  - `stream` boolean
  - `historyCount` number
  - `fileIds` string[]
  - `wait` boolean
  - `enableTools` boolean
  - `toolMode` 'fast' | 'smart'
  - `tools` string[]: Explicit tool ids to offer the model. A non-empty array enables tools on its own; no companion `toolMode` or `enableTools` is required. Merged with the auto-selected set under `toolMode: "smart"`, and ignored under `toolMode: "fast"`. This list ADDS to what is offered rather than restricting it - the server still offers tools of its own (for example knowledge retrieval when the session has reachable documents). Unrecognized ids are dropped rather than rejecting the request; the response reports the surviving set as `tools.effectiveTools` and the ids that are not tools in this deployment as `tools.unrecognizedTools`, since no endpoint enumerates the valid ids. `unrecognizedTools` is reported under every `toolMode` - it describes the ids, not what the mode did with them - so under `toolMode: "fast"` an id can be absent from `effectiveTools` (the mode discarded it) without being unrecognized. Both reported lists are deduplicated, and `unrecognizedTools` names at most the first 10 distinct ids.
  - `enableQuestMaster` boolean
  - `enableMementos` boolean
  - `enableAgents` boolean
  - `promptMode` 'raw' | 'grounded' | 'surface'
  - `skip_auto_offers` boolean: Suppress tools the server would otherwise attach on its own for this session (the knowledge-base search offer, in-app view navigation, blog drafting/editing/publishing, skill invocation, and every tool from a connected MCP server). Native tools you request explicitly in `tools` are unaffected, but an MCP server tool cannot be named through this field, so with this flag set a caller with an MCP server connected sees none of its tools regardless of `tools`. One system-prompt block goes with them: withholding in-app view navigation also drops the view-registry block that exists only to describe it. No other prompt content changes. This does not switch off retrieval: a session with forced knowledge retrieval still retrieves, and documents already attached to the session are still placed in the prompt directly. Any promptMode suppresses these too, so false has no effect alongside one.
  - `includePromptDetails` boolean
  - `includeSystemPrompt` boolean
  - `systemPrompt` string: System-prompt text for this request only, never persisted. Rendered as a defended block appended after every other system-prompt source, with prose instructing the model to defer to organization, session and data-lake guidance. Over the cap is a 422, never truncated.

## Response `200`

Message accepted - NOT a completed turn. The default (async) path returns this queued ACK; the outcome arrives on `GET /api/v1/quests/{id}` (see the `sendChatMessage200PollResult` schema). With `wait: true` the body is instead the completed turn: the ACK fields plus the reply (`response`/`responses`), `toolPayloads`, `createdAt`, `performance` timings and, when requested, `promptDetails`/`promptText`. A turn that FAILS still resolves with `200`, never a 4xx, on both that `wait: true` body and the polled quest (`GET /api/v1/quests/{id}`) - the prose explaining why lands in `reply`/`response` like any other answer, so the reply text alone cannot tell a failure from an answer. `type` is the field that can: both surfaces carry it unconditionally, so match on `type: "error"` first - it covers credit exhaustion, a provider timeout or overload, and an in-process aborted turn; a real answer carries the turn's actual completion type instead (`"message"` for an ordinary reply). Two related states do NOT set `type: "error"`: a user-cancelled turn resolves as `status: "stopped"` with `type` left at `"message"`, and a recovered stuck quest that still has renderable content resolves as a success (`status: "done"`, no error) by design, to avoid destroying content to report a failure. `errorCode` then names the failure reason, but only for the billing failures that have one - `"insufficient_credits"` today; it is absent on every other `type: "error"` turn, so never use its absence to infer success. On a real answer `errorCode` is absent from the `wait: true` body. The body also carries `sessionId`, the notebook the turn was recorded in - for an API-key caller that sent none (or any caller sending `newConversation: true`), that is the freshly created notebook's id; pass it back as the request's `sessionId` to continue. Contrast the tts/music/soundEffects contracts, which reject synchronously with a 422 carrying the same `errorCode` vocabulary.

- union
  - object
    - `id` string, required
    - `status` string, required
    - `message_received` boolean, required
    - `timestamp` string, required
    - `model` string, required
    - `sessionId` string
    - `message` string
    - `type` 'message' | 'oob' | 'error' | 'system' | 'voice_transcript', required
    - `errorCode` 'insufficient_credits' | 'spend_cap_exceeded'
    - `tools` object
      - `toolMode` 'fast' | 'smart'
      - `autoSelectedTools` string[]
      - `effectiveTools` string[], required
      - `unrecognizedTools` string[]
    - `tracking_info` object, required
      - `quest_id` string, required
      - `check_status_url` string, required
      - `poll_url` string
    - `response` string, nullable, required
    - `responses` string[], required
    - `toolPayloads` object[], required
      - `type` string, required
      - `payload` unknown
    - `createdAt` union, required
      - string, date-time
      - string, date-time
    - `promptDetails` object[]
      - `source` 'hardcoded' | 'admin' | 'user' | 'project' | 'session' | 'org' | 'caller', required
      - `name` string, required
      - `tokenCount` number, required
      - `wasIncluded` boolean, required
      - `exclusionReason` 'duplicate' | 'disabled' | 'token_limit'
    - `promptText` string
    - `performance` object, required
      - `total_ms` number, required
      - `phases` object, required
      - `pipeline_phases` object
  - object
    - `id` string, required
    - `status` string, required
    - `message_received` boolean, required
    - `timestamp` string, required
    - `model` string, required
    - `sessionId` string
    - `message` string
    - `type` 'message' | 'oob' | 'error' | 'system' | 'voice_transcript'
    - `errorCode` 'insufficient_credits' | 'spend_cap_exceeded'
    - `tools` object
      - `toolMode` 'fast' | 'smart'
      - `autoSelectedTools` string[]
      - `effectiveTools` string[], required
      - `unrecognizedTools` string[]
    - `tracking_info` object, required
      - `quest_id` string, required
      - `check_status_url` string, required
      - `poll_url` string

## Other responses

- `400`: The request was rejected before a turn was created: a supplied `model` is unknown, disabled or not a chat model; or no usable default chat model is configured and none was supplied.
- `401`: Missing or invalid credentials.
- `403`: The API key does not hold any of the required scopes.
- `404`: No notebook/session exists to attach the message to.
- `405`: The path does not serve this HTTP method.
- `422`: Request body failed schema validation, or `sessionId` and `newConversation: true` were sent together (they are mutually exclusive).
- `429`: Per-user rate limit exceeded.
- `504`: A `wait: true` turn ran past the request deadline. The turn is already ended: its quest (`quest_id`) is settled as timed out with any partial reply kept, and unused reserved credits are refunded. Retry in async mode for a long turn.

---

[API](https://skmtc.dev/bike4mind/apis/bike4mind-api.md) · [All operations](https://skmtc.dev/bike4mind/apis/bike4mind-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bike4mind/apis/bike4mind-api/revisions/db47495b892f?raw)
