> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/tech/en/api-integration/dui-hua-yu-xun-xi-hui-fu-chuan-liu-tong-bu.md).

# Conversation and Message Responses (Streaming/Synchronous)

Call the completions API for Agent replies, with streaming (SSE) event fields, reset handling, and completion conditions

Send a message to an Agent and receive a response. When `isStreaming` is `false`, the complete reply is returned as a single JSON response; when `true`, it is returned as it is generated using Server-Sent Events (SSE).

```bash
curl -N -X POST \
  'https://api.maiagent.ai/api/v1/chatbots/{chatbot_id}/completions/' \
  -H 'Authorization: Api-Key YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "conversation": null,
    "message": {
      "content": "Hello, please introduce yourself"
    },
    "isStreaming": true
  }'
```

**Parameters:**

* `chatbot_id` (path parameter): Agent ID
* `conversation` (request body): Conversation ID. Pass `null` or omit it to create a new conversation; pass an existing ID to continue that conversation
* `message.content` (request body): The message content to send
* `message.attachments` (request body): Attachment array (optional)
* `isStreaming` (request body): Whether to stream the response. Defaults to `false` (`is_streaming` is also accepted)

For other request fields (`queryMetadata`, `toolIds`, `waitForAttachments`, etc.), see [Conversations and Messages](https://docs.maiagent.ai/api/api-reference/dui-hua-he-xun-xi) on the API documentation site.

**Response (non-streaming):**

```json
{
  "conversationId": "string",
  "content": "Complete reply content",
  "done": true,
  "reset": false,
  "citations": null,
  "citationNodes": null,
  "contentPayload": {
    "content": "Complete reply content",
    "items": [{ "type": "text", "text": "Complete reply content" }],
    "metadata": null
  },
  "messageId": "string"
}
```

The fields have the same meanings as in the streaming events below.

## Streaming Response Format <a href="#streaming-format" id="streaming-format"></a>

When `isStreaming: true`, the response `Content-Type` is `text/event-stream`. Each event contains one `data: <JSON>` line, with a blank line separating events:

```
data: {"conversationId": "c6a4…", "content": "Hello", "done": false, "reset": false, "citations": null, "citationNodes": null, "contentPayload": {"content": "Hello", "items": [{"type": "text", "text": "Hello", …}], "metadata": null}, "messageId": null}

data: {"conversationId": "c6a4…", "content": ", I am", "done": false, "reset": false, …, "messageId": null}

data: {"conversationId": "c6a4…", "content": "", "done": true, "reset": false, "citations": […], "citationNodes": […], "contentPayload": {}, "messageId": "9f1e…"}
```

### Event Fields <a href="#frame-fields" id="frame-fields"></a>

All field names use camelCase.

| Field            | Type             | Description                                                                                                                                                                                                                                                  |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `conversationId` | string           | Conversation ID. Retrieve it here when creating a new conversation, then pass it as `conversation` in subsequent requests to continue the conversation                                                                                                       |
| `content`        | string           | The text fragment **added** by this event (a delta, not the accumulated full text). For thinking, tool call, citation, and heartbeat events, `content` is an empty string                                                                                    |
| `done`           | boolean          | `true` indicates the final event, after which the server closes the connection                                                                                                                                                                               |
| `reset`          | boolean          | When `true`, **replace all** currently displayed text with this event's `content`; see [When reset Occurs](#reset). A few events omit this field; treat it as `false`                                                                                        |
| `citations`      | array or `null`  | Knowledge base files cited in the reply. Only populated in a `done: true` event, and only if citation display is enabled on the Agent's API conversation platform; otherwise `null`                                                                          |
| `citationNodes`  | array or `null`  | Passages cited in the reply. Populated in a `done: true` event; with inline citations enabled, retrieval may also be followed by an earlier event with empty `content` and only `citationNodes`                                                              |
| `contentPayload` | object           | Structured content. Each entry in the `items` array is distinguished by `type`, such as `text`, `thinking` (requires thinking display to be enabled), `tool_use`, `tool_result`, or `client_tool_call`. Integrations that only need text can skip this field |
| `messageId`      | string or `null` | ID of the AI reply message. Populated only after the reply is saved, so it is `null` during streaming and usually appears only in the `done: true` event                                                                                                     |

### Frontend Handling Rules <a href="#client-handling" id="client-handling"></a>

Process each event in order:

1. If `reset` is `true`: replace the accumulated text with this event's `content` (`content` may be an empty string, meaning clear the text).
2. For other events: append `content` to the accumulated text. Append empty `content` as usual; no special check is needed.
3. If `done` is `true`: finish after processing this event and retrieve `messageId`, `citations`, and `citationNodes` from it.

In a `done: true` event, `content` is usually an empty string because the text has already been sent in earlier events. It contains text when no fragments were streamed for the entire reply (for example, when the input was blocked), in which case `content` is the complete reply. When a Hook rewrites the reply, the event contains `reset: true` and the full content. The rules above handle both cases correctly.

### When reset Occurs <a href="#reset" id="reset"></a>

* **An output Hook rewrites or blocks the reply**: the final event has `done: true` and `reset: true`, with `content` containing the complete rewritten content or a blocking notice. If the frontend only appends `content`, the original content already streamed (including blocked content) remains on screen.
* **The model regenerates the reply**: an event with `reset: true` and empty `content` is sent during streaming to clear the displayed text. Subsequent events contain regenerated content (for example, an error explanation replacing abnormal model output).

{% hint style="warning" %}
You must handle `reset`. An integration that only appends `content` leaves content that should not be displayed on the user's screen when a Hook blocks the reply.
{% endhint %}

### Heartbeats <a href="#heartbeat" id="heartbeat"></a>

During streaming, the server sends an event with empty `content` and `done: false` every 15 seconds to prevent an intermediary proxy or load balancer from closing the connection as idle. Simply append the empty string according to the handling rules.

### Completion Conditions and Errors <a href="#end-and-errors" id="end-and-errors"></a>

* **Normal completion**: a `done: true` event is received, and the server then closes the connection.
* **Error or timeout while generating the reply**: in most cases, an error or timeout explanation is sent as reply text, ending with `done: true`. The handling rules display it correctly.
* **Unexpected error**: the server sends an `event: error` event and closes the connection. It does **not** send `done: true` afterward:

  ```
  event: error
  data: {"error": "Internal server error", "conversationId": "c6a4…"}
  ```
* **Connection closes early**: if the connection closes before `done: true` or `event: error` is received, treat the response as incomplete. The server still finishes generating and saves the reply. Later, query the conversation's messages with `GET /api/v1/messages/?conversation={conversationId}` to retrieve the result.

### Cases Without Incremental Streaming <a href="#non-streaming-cases" id="non-streaming-cases"></a>

Even when `isStreaming` is `true`, responses in the following cases are not incremental SSE:

| Case                                                                                                          | Response                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The request is blocked before reply generation, for example due to validation failure or insufficient Credits | A regular JSON error response with the corresponding 4xx status code, not SSE                                                                                                                                                                                               |
| Attachments are still being indexed                                                                           | HTTP `202`, with JSON containing `status: "pending_attachments"`, `conversationId`, `message`, and `pendingAttachments` (IDs of attachments not yet ready), not SSE                                                                                                         |
| The conversation has been transferred to a human support agent                                                | SSE contains only one `done: true` event, with `content` containing a queue or transfer notice, `messageId` set to `null`, and an additional `progressStatus`. See [Third-Party Customer Service System Integration](/tech/en/serve/customer-service-system-integration.md) |
| The Agent is configured for structured JSON output                                                            | The full reply is generated first. SSE contains only one `done: true` event, with `content` containing the complete reply                                                                                                                                                   |

### Code Examples <a href="#examples" id="examples"></a>

The examples below accumulate reply text, handle `reset` and `event: error`, and return the result when `done: true`. Run them on your own backend; do not put the API Key in the browser frontend.

{% tabs %}
{% tab title="Python" %}
Requires `requests` (`pip install requests`).

```python
import json

import requests

API_KEY = "YOUR_API_KEY"
CHATBOT_ID = "YOUR_CHATBOT_ID"


def stream_reply(content, conversation_id=None):
    response = requests.post(
        f"https://api.maiagent.ai/api/v1/chatbots/{CHATBOT_ID}/completions/",
        headers={"Authorization": f"Api-Key {API_KEY}"},
        json={
            "conversation": conversation_id,
            "message": {"content": content},
            "isStreaming": True,
        },
        stream=True,
        timeout=(10, 300),
    )
    response.raise_for_status()
    if response.status_code == 202:
        raise RuntimeError(f"Attachments have not been indexed: {response.json()}")

    text = ""
    event = "message"
    # The response does not declare a charset; read lines as bytes and decode them as UTF-8
    for raw_line in response.iter_lines(chunk_size=None):
        line = raw_line.decode("utf-8")
        if not line:
            event = "message"
            continue
        if line.startswith("event:"):
            event = line[len("event:"):].strip()
            continue
        if not line.startswith("data:"):
            continue

        frame = json.loads(line[len("data:"):])
        if event == "error":
            raise RuntimeError(f"Streaming error: {frame['error']} (conversation {frame['conversationId']})")

        if frame.get("reset"):
            text = frame["content"]
        else:
            text += frame["content"]

        if frame["done"]:
            return {
                "conversation_id": frame["conversationId"],
                "message_id": frame.get("messageId"),
                "text": text,
                "citations": frame.get("citations"),
            }

    raise RuntimeError("Connection closed before receiving done")


result = stream_reply("Hello, please introduce yourself")
print(result["text"])
```

{% endtab %}

{% tab title="JavaScript (Node.js 18+)" %}
Uses the built-in `fetch`; no package installation is needed.

```javascript
const API_KEY = "YOUR_API_KEY";
const CHATBOT_ID = "YOUR_CHATBOT_ID";

async function streamReply(content, conversationId = null, onUpdate = () => {}) {
  const response = await fetch(
    `https://api.maiagent.ai/api/v1/chatbots/${CHATBOT_ID}/completions/`,
    {
      method: "POST",
      headers: {
        Authorization: `Api-Key ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        conversation: conversationId,
        message: { content },
        isStreaming: true,
      }),
    },
  );
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  }
  if (response.status === 202) {
    throw new Error(`Attachments have not been indexed: ${await response.text()}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";
  let text = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let boundary;
    while ((boundary = buffer.indexOf("\n\n")) !== -1) {
      const rawEvent = buffer.slice(0, boundary);
      buffer = buffer.slice(boundary + 2);

      let eventName = "message";
      let data = "";
      for (const line of rawEvent.split("\n")) {
        if (line.startsWith("event:")) eventName = line.slice(6).trim();
        else if (line.startsWith("data:")) data += line.slice(5).trim();
      }
      if (!data) continue;

      const frame = JSON.parse(data);
      if (eventName === "error") {
        throw new Error(`Streaming error: ${frame.error} (conversation ${frame.conversationId})`);
      }

      // Replace the entire text when reset is true; otherwise append
      text = frame.reset ? frame.content : text + frame.content;
      onUpdate(text);

      if (frame.done) {
        return {
          conversationId: frame.conversationId,
          messageId: frame.messageId,
          text,
          citations: frame.citations,
        };
      }
    }
  }
  throw new Error("Connection closed before receiving done");
}

streamReply("Hello, please introduce yourself").then((result) => console.log(result.text));
```

`onUpdate` receives the current full text for every event and can be used to update the UI.
{% endtab %}
{% endtabs %}

Notes:

* Use the response's `conversationId` as `conversation` in subsequent requests to continue the same conversation
* A connection interruption during streaming does not cancel the server-side reply. Do not immediately resend the same message to "retry", as this creates duplicate messages in the conversation

## Events During Tool Calls <a href="#tool-call-frames" id="tool-call-frames"></a>

In Agent mode, the model may call tools before answering. Streaming and synchronous responses behave as follows:

* **Streaming**: tool calls are sent as `tool_use` and `tool_result` entries in `contentPayload.items`; these events have empty `content`. Text generated by the model before and after tool calls is sent as `content` deltas. Append `content` according to the [Frontend Handling Rules](#client-handling) to obtain the entire reply.
* **Synchronous** (`isStreaming: false`): `content` contains the entire reply text, including text before and after tool calls; the tool call process is listed in `contentPayload.items` in chronological order.
* There is no parameter to return only the final text segment. To display only the final answer, take the last `type: "text"` entry in `contentPayload.items`.
* `done: true` always marks the end of the entire turn (including all tool calls); it does not appear midway through tool calls.

The maximum number of tool call rounds and the timeout are determined by the Agent's advanced settings. See [Function Calling: Execution Limits](/tech/en/advanced-genai-tech/function-calling.md#limits).

## Using Code Interpreter via the API <a href="#code-interpreter" id="code-interpreter"></a>

Code Interpreter is not a tool ID; do not put it in `message.toolIds`. It is an Agent setting: enable it in the admin console (see [Code Interpreter](https://docs.maiagent.ai/build/agent/code-interpreter#what-do-i-need-to-do) in the user manual), or set `enableCodeInterpreter` to `true` using `PATCH /api/v1/chatbots/{chatbotId}/`. The Agent's `replyMode` must be `agent`. Once configured, `completions` requests to this Agent need no extra fields; the model decides when to execute code. Neither Canvas nor Agent Teams is a prerequisite.

Generated files do not appear as attachment objects in the reply; see [Conversation Outputs List API](/tech/en/api-integration/endpoint-behaviors/conversation-outputs-api.md) for how to retrieve them. `/api/v1/chatbots/{chatbotId}/completions/` is the Agent conversation endpoint. The similarly named `/api/v1/chat/completions/` is the OpenAI-compatible [AI Gateway](/tech/en/ai-gateway/ai-gateway-openai-compatible.md) endpoint, which calls models directly without applying Agent settings and does not provide Code Interpreter.

## Identity When `message.sender` Is Omitted <a href="#anonymous-sender" id="anonymous-sender"></a>

Set `message.sender` to a contact ID to specify who is asking. Omitting it does not mean anonymous access; instead:

* When **creating a new conversation** (`conversation` is `null`), the conversation's contact is the contact on this conversation platform for the **member bound to the API key** (automatically created on the first call, with `sourceId` set to that member's user ID). All conversations created with the same key and without `sender` belong to this contact: conversation history, persistent memory, the contact's `queryMetadata`, and tool credentials are based on it. Usage and Credits are also attributed to this member.
* When **continuing an existing conversation** (passing `conversation`), the original contact remains unchanged; `sender` does not change it.

To give each end user their own conversation history and permissions, create contacts first and pass `sender` with every request. See [Contact Introduction and Integration](/tech/en/serve/contacts.md).

## Content Length and Context <a href="#content-limits" id="content-limits"></a>

* `message.content` is a string, with no additional character limit set by the platform. It may be empty when attachments are present, but must not be empty without attachments (returns `400`).
* The actual limit is the model's context window, which includes this message, conversation history, retrieved knowledge base passages, text parsed from attachments, and role instructions. If exceeded, the model returns an error; the platform sends the error explanation as reply text and ends with `done: true` (see [Completion Conditions and Errors](#end-and-errors)), rather than returning `413`.
* Upload long text as an attachment or split it into multiple messages. Reply length is controlled by the Agent's "Reply Length and Agent Execution Limits" setting; see [Reply Length and Agent Execution Limits](https://docs.maiagent.ai/build/agent/custom-max-tokens) in the user manual.

## Conversation ID Lifetime and Errors <a href="#conversation-lifecycle" id="conversation-lifecycle"></a>

* Conversations have no expiration time or limit on the number of turns. As long as the conversation has not been deleted, you can keep using `conversationId`. Conversation attachments have a separate retention period; attachment files are deleted when it expires. See [Conversation Attachment Management API](/tech/en/api-integration/endpoint-behaviors/conversation-attachments.md).
* Passing a nonexistent or deleted ID as `conversation` returns `400` with a field error: `{"conversation": ["Invalid pk \"…\" - object does not exist."]}`. Conversations outside the organization associated with this key are also rejected. When you receive this error, create a new conversation with `null` instead of retrying the same ID.
* If the conversation you continue has been transferred to a human support agent, the response is not a standard AI reply; see [Cases Without Incremental Streaming](#non-streaming-cases).

## Writing Human Replies to a Conversation <a href="#outgoing-message" id="outgoing-message"></a>

To write replies from human agents in your own customer service system to a MaiAgent conversation, use `POST /api/v1/messages/outgoing/` instead of the Completions endpoint for asking the AI questions. For required permissions, fields, request examples, and the human handoff lifecycle, see [Third-Party Customer Service System Integration: Integration Flow](/tech/en/serve/customer-service-system-integration.md#scenario-b-flow).

## Integration Result

{% embed url="<https://drive.google.com/file/d/1sCIoxxASZ-SnHPAmkjDT270B3dz0LyHo/view?usp=sharing>" %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.maiagent.ai/tech/en/api-integration/dui-hua-yu-xun-xi-hui-fu-chuan-liu-tong-bu.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
