> ## Documentation Index
> Fetch the complete documentation index at: https://www.meilisearch.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Chats API

> Use the experimental `/chats` API directly to set up conversational search, stream completions, and inspect Meilisearch chat tools.

This page documents direct use of the Meilisearch `/chats` API: an engine-managed endpoint that consolidates retrieval, context management, and generation into a single call. For most agentic and conversational search projects, start with the [AI SDK getting started guide](/docs/capabilities/agentic_search/getting_started) instead, and come back to this page only if you specifically need to call `/chats` directly.

<Note>
  In code examples, replace `WORKSPACE_NAME` with the name of your workspace. On Meilisearch Cloud, the default workspace name is `cloud`.
</Note>

## Setup

Enable the feature, configure your indexes, and create a workspace before you send chat completions.

### Enable the chat completions feature

Enable chat completions from your Meilisearch Cloud project in one of two ways:

* Go to your project's **Settings** page and enable it under **Experimental features**
* Or open the **Chat** tab in your project and activate the feature directly from there

<Note>
  For self-hosted instances, enable the feature through the [experimental features API](/docs/reference/api/experimental-features/configure-experimental-features) by sending a `PATCH` request with `chatCompletions` set to `true`:

  <CodeGroup>
    ```bash cURL theme={null}
    curl \
      -X PATCH 'MEILISEARCH_URL/experimental-features/' \
      -H 'Content-Type: application/json' \
      --data-binary '{
        "chatCompletions": true
      }'
    ```
  </CodeGroup>
</Note>

### Find your chat API key

Meilisearch automatically generates a "Default Chat API Key" that combines `chatCompletions` and `search` permissions on all indexes. Conversational search requires both actions: `chatCompletions` authorizes the LLM call, and `search` authorizes the retrieval step that feeds documents to the model. Any key you use with the `/chats` routes must carry both actions, so prefer the default chat API key unless you have a specific reason to create a custom one.

Check if you have the key using:

<CodeGroup>
  ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  curl \
    -X GET 'MEILISEARCH_URL/keys' \
    -H 'Authorization: Bearer MASTER_KEY'
  ```

  ```javascript JS theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  const client = new MeiliSearch({ host: 'MEILISEARCH_URL', apiKey: 'masterKey' })
  client.getKeys()
  ```

  ```python Python theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  client = Client('MEILISEARCH_URL', 'masterKey')
  client.get_keys()
  ```

  ```php PHP theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  $client = new Client('MEILISEARCH_URL', 'masterKey');
  $client->getKeys();
  ```

  ```java Java theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  Client client = new Client(new Config("MEILISEARCH_URL", "masterKey"));
  client.getKeys();
  ```

  ```ruby Ruby theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  client = MeiliSearch::Client.new('MEILISEARCH_URL', 'masterKey')
  client.keys
  ```

  ```go Go theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  client := meilisearch.New("MEILISEARCH_URL", meilisearch.WithAPIKey("masterKey"))
  client.GetKeys(nil);
  ```

  ```csharp C# theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  MeilisearchClient client = new MeilisearchClient("MEILISEARCH_URL", "masterKey");
  var keys = await client.GetKeysAsync();
  ```

  ```rust Rust theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  let client = Client::new("MEILISEARCH_URL", Some("MASTER_KEY")); let keys = client .get_keys() .await .unwrap();
  ```

  ```swift Swift theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  client = try MeiliSearch(host: "MEILISEARCH_URL", apiKey: "masterKey")
  client.getKeys { result in
      switch result {
      case .success(let keys):
          print(keys)
      case .failure(let error):
          print(error)
      }
  }
  ```

  ```dart Dart theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  var client = MeiliSearchClient('MEILISEARCH_URL', 'masterKey');
  await client.getKeys();
  ```
</CodeGroup>

Look for the key with the description "Default Chat API Key".

Chat queries only search the indexes that the API key can access. The default chat API key is scoped to all indexes. To limit which indexes a chat client can reach, you have two options:

* Create a new API key with both `chatCompletions` and `search` actions, scoped to the exact indexes you want exposed. See [manage API keys](/docs/capabilities/security/how_to/manage_api_keys) for the full workflow.
* Generate a [tenant token](/docs/capabilities/security/how_to/generate_token_from_scratch) from the default chat API key. Tenant tokens inherit both the `chatCompletions` and `search` actions from their parent key and let you narrow index access or attach search rules per user.

<Note>
  A tenant token cannot grant access to an index its parent API key does not already cover. Make sure the parent key is scoped to every index the token should be allowed to reach.
</Note>

If your instance does not have a Default Chat API Key, create one manually:

<CodeGroup>
  ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  curl \
    -X POST 'MEILISEARCH_URL/keys' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "name": "Chat API Key",
      "description": "API key for chat completions",
      "actions": ["search", "chatCompletions"],
      "indexes": ["*"],
      "expiresAt": null
    }'
  ```
</CodeGroup>

### Configure your indexes

Configure chat settings for each index you want the agent to search:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/chat' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "description": "A movie database containing titles, genres, release dates, keywords, and plot overviews to help users find films to watch",
      "documentTemplate": "A movie titled '\''{{doc.title}}'\'' that released in {{ doc.release_date | date: '\''%Y'\'' }}. The movie genres are: {{doc.genres}}. The key themes include: {{doc.keywords}}. The storyline is about: {{doc.overview|truncatewords: 100}}",
      "documentTemplateMaxBytes": 400
    }'
  ```
</CodeGroup>

* `description` tells the LLM what the index contains
* `documentTemplate` is a [Liquid](https://shopify.github.io/liquid/) template that defines the text sent to the LLM for each document
* `documentTemplateMaxBytes` truncates the rendered template. The default of 400 bytes balances context quality and speed

See [index chat settings](#index-chat-settings) for the remaining fields, then consult [optimize chat prompts](/docs/capabilities/agentic_search/how_to/optimize_chat_prompts) and [document template best practices](/docs/capabilities/hybrid_search/advanced/document_template_best_practices) for tuning.

### Configure a workspace

A workspace holds your LLM provider configuration and system prompt. The model itself is chosen per request in the `/chat/completions` call, not in the workspace settings.

On Meilisearch Cloud, your project comes with a single default workspace named `cloud`. Use `cloud` as `WORKSPACE_NAME` in all API calls. If you need additional workspaces, contact us.

On self-hosted instances, you can create as many workspaces as you need. If the workspace does not exist, Meilisearch creates it when you first `PATCH` its settings:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "source": "openAi",
      "apiKey": "PROVIDER_API_KEY",
      "prompts": {
        "system": "You are a helpful assistant. Answer questions based only on the provided context."
      }
    }'
  ```
</CodeGroup>

`baseUrl` is required for all providers except OpenAI. See [workspace settings](#workspace-settings) for the provider matrix, then [configure guardrails](/docs/capabilities/agentic_search/how_to/configure_guardrails) and [optimize chat prompts](/docs/capabilities/agentic_search/how_to/optimize_chat_prompts) for the system prompt.

## Chat patterns

Both patterns use `POST /chats/{workspace}/chat/completions`. The difference is the prompt strategy and whether you keep conversation history.

### Chat interface

Build a multi-turn interface where users ask follow-up questions. Meilisearch searches your indexes, then passes the retrieved documents to the LLM to generate a grounded response.

#### Streaming is required

All requests to the chat completions endpoint must include `"stream": true`. Non-streaming (`stream: false`) is not yet supported and returns a `501 Not Implemented` error.

#### Message roles

Every entry in the `messages` array carries a `role` that tells the LLM who authored it:

| Role        | Origin       | Typical content                                                                                                                                                                          |
| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `system`    | Meilisearch  | Workspace-level instructions (the `prompts.system` string) plus internal tool descriptions injected by Meilisearch. You do not need to send this role yourself. Meilisearch prepends it. |
| `assistant` | LLM provider | Responses generated by the configured model, including tool calls. Push these back into `messages` to preserve context on follow-up turns.                                               |
| `user`      | User input   | Questions and follow-ups coming from the end user of your application.                                                                                                                   |

#### Send a streaming request

Send a `POST` request to `/chats/{workspace}/chat/completions` with `stream: true`:

<CodeGroup>
  ```bash cURL theme={null}
  curl -N \
    -X POST 'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "model": "PROVIDER_MODEL_UID",
      "stream": true,
      "messages": [
        {
          "role": "user",
          "content": "What movies are about artificial intelligence?"
        }
      ]
    }'
  ```

  ```javascript OpenAI SDK theme={null}
  import OpenAI from 'openai';

  const client = new OpenAI({
    baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
    apiKey: 'MEILISEARCH_KEY',
  });

  const stream = await client.chat.completions.create({
    model: 'PROVIDER_MODEL_UID',
    stream: true,
    messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    process.stdout.write(content);
  }
  ```

  ```javascript Vercel AI SDK theme={null}
  import { createOpenAI } from '@ai-sdk/openai';
  import { streamText } from 'ai';

  const meilisearch = createOpenAI({
    baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
    apiKey: 'MEILISEARCH_KEY',
  });

  const { textStream } = streamText({
    model: meilisearch('PROVIDER_MODEL_UID'),
    messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
  });

  for await (const text of textStream) {
    process.stdout.write(text);
  }
  ```
</CodeGroup>

This basic request works, but without Meilisearch tools you get no visibility into what is being searched and no way to keep conversation context across follow-up questions. Declare the three tools listed in [tools reference](#tools-reference).

`_meiliAppendConversationMessage` is the key to multi-turn conversations. The endpoint is stateless, so Meilisearch uses this tool to expose internal search tool calls and their results back to the client. Push those messages into your `messages` array before the next request, or the LLM loses the context from previous searches.

#### Complete example: progress, sources, and history

The following example combines all three tools: streaming progress, displaying sources, and maintaining conversation history.

```javascript JavaScript (Fetch) theme={null}
const MEILISEARCH_TOOLS = [
  {
    type: 'function',
    function: {
      name: '_meiliSearchProgress',
      description: 'Provides information about the current Meilisearch search operation',
      parameters: {
        type: 'object',
        properties: {
          call_id: { type: 'string' },
          function_name: { type: 'string' },
          function_parameters: { type: 'string' },
        },
        required: ['call_id', 'function_name', 'function_parameters'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
  {
    type: 'function',
    function: {
      name: '_meiliSearchSources',
      description: 'Provides sources of the search',
      parameters: {
        type: 'object',
        properties: {
          call_id: { type: 'string' },
          documents: { type: 'array', items: { type: 'object' } },
        },
        required: ['call_id', 'documents'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
  {
    type: 'function',
    function: {
      name: '_meiliAppendConversationMessage',
      description: 'Append a new message to the conversation based on what happened internally',
      parameters: {
        type: 'object',
        properties: {
          role: { type: 'string' },
          content: { type: 'string' },
          tool_calls: { type: ['array', 'null'] },
          tool_call_id: { type: ['string', 'null'] },
        },
        required: ['role', 'content', 'tool_calls', 'tool_call_id'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

const messages = [];

async function chat(userMessage) {
  messages.push({ role: 'user', content: userMessage });

  const response = await fetch('MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer MEILISEARCH_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'PROVIDER_MODEL_UID',
      stream: true,
      messages,
      tools: MEILISEARCH_TOOLS,
    }),
  });

  const reader = response.body?.getReader();
  if (!reader) throw new Error('No readable stream on response');
  const decoder = new TextDecoder();
  let answer = '';
  let buffer = '';
  const pendingToolCalls = {};

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() ?? '';

    for (const line of lines) {
      if (!line.startsWith('data: ') || line === 'data: [DONE]') continue;

      const chunk = JSON.parse(line.slice(6));
      const delta = chunk.choices[0]?.delta;

      if (delta?.content) {
        answer += delta.content;
        process.stdout.write(delta.content);
      }

      for (const toolCall of delta?.tool_calls ?? []) {
        if (toolCall.id) {
          pendingToolCalls[toolCall.index] = { name: toolCall.function.name, args: '' };
        }
        const pending = pendingToolCalls[toolCall.index];
        if (pending && toolCall.function?.arguments) {
          pending.args += toolCall.function.arguments;
        }
      }
    }
  }

  for (const call of Object.values(pendingToolCalls)) {
    const args = JSON.parse(call.args);

    if (call.name === '_meiliSearchProgress') {
      const params = JSON.parse(args.function_parameters);
      console.log(`Searched "${params.q}" in index "${params.index_uid}"`);
    }

    if (call.name === '_meiliSearchSources') {
      console.log('Sources used:', args.documents);
    }

    if (call.name === '_meiliAppendConversationMessage') {
      messages.push(args);
    }
  }

  messages.push({ role: 'assistant', content: answer });
}

await chat('What movies are about artificial intelligence?');
await chat('Which one has the best reviews?');
```

For UI patterns that surface those source documents, see [display source documents](/docs/capabilities/agentic_search/how_to/display_source_documents).

### Summarized answers

One-shot summarization uses the same `/chats` API, but with a different prompt strategy: you send a single question and receive a concise answer based on your indexed documents. This is useful for displaying an AI-generated answer alongside traditional search results.

Configure a dedicated workspace prompt that asks for short, self-contained answers and avoids follow-up questions:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "prompts": {
        "system": "You are a search assistant. When the user asks a question, provide a single concise answer based only on the search results. Keep your response to 2-3 sentences maximum. Do not ask follow-up questions. Do not use your general knowledge. If the search results do not contain enough information, say so briefly."
      }
    }'
  ```
</CodeGroup>

Use a dedicated workspace for summarization rather than reusing a chat workspace, so the prompts stay separate. On Meilisearch Cloud, if you need a second workspace, contact support.

Send one user message and do not maintain conversation history:

<CodeGroup>
  ```bash cURL theme={null}
  curl -N \
    -X POST 'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "model": "PROVIDER_MODEL_UID",
      "stream": true,
      "messages": [
        {
          "role": "user",
          "content": "What is the return policy for electronics?"
        }
      ],
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "_meiliSearchSources",
            "description": "Provides sources of the search",
            "parameters": {
              "type": "object",
              "properties": {
                "call_id": { "type": "string", "description": "The call ID to track the original search" },
                "documents": { "type": "array", "items": { "type": "object" }, "description": "The documents associated with the search" }
              },
              "required": ["call_id", "documents"],
              "additionalProperties": false
            },
            "strict": true
          }
        }
      ]
    }'
  ```
</CodeGroup>

Including `_meiliSearchSources` lets you display the source documents next to the summary. In a real application, run this in parallel with a standard Meilisearch search request and display both results together.

For more deterministic answers, pass a lower `temperature` (for example, `0.1` or `0.2`). Meilisearch [forwards these parameters](#llm-provider-parameters-passthrough) to your LLM provider.

## Configuration shortcuts

Workspace settings connect Meilisearch to an LLM. Index chat settings describe each index to the agent. Both are required.

### Workspace settings

Create or update a workspace with `PATCH /chats/{workspace_uid}/settings`. If the workspace does not exist, Meilisearch creates it.

The `source` field selects the LLM provider:

| Provider     | `source` value | Required fields                                         | Optional fields |
| ------------ | -------------- | ------------------------------------------------------- | --------------- |
| OpenAI       | `openAi`       | `apiKey`                                                | `baseUrl`       |
| Azure OpenAI | `azureOpenAi`  | `apiKey`, `baseUrl`, `orgId`, `projectId`, `apiVersion` | `deploymentId`  |
| Mistral      | `mistral`      | `apiKey`, `baseUrl`                                     |                 |
| vLLM         | `vLlm`         | `baseUrl`                                               | `apiKey`        |

A few provider-specific rules:

* `orgId`, `projectId`, and `apiVersion` are required for Azure OpenAI and are incompatible with every other `source`
* `baseUrl` is required for Azure OpenAI and vLLM. For Mistral it points to the Mistral API endpoint. For OpenAI it is optional and only needed when routing through a custom endpoint
* `apiKey` is optional for vLLM and mandatory for every other provider

<Warning>
  The `apiKey` field is write-only. Meilisearch stores it for outbound LLM calls but redacts it in every response from the workspace settings endpoint. To rotate the key, `PATCH` the workspace with the new value.
</Warning>

Set baseline agent instructions with `prompts.system`. The `prompts` object also accepts tool-facing fields (`searchDescription`, `searchQParam`, `searchFilterParam`, `searchIndexUidParam`) that help the LLM decide when and how to search. See [optimize chat prompts](/docs/capabilities/agentic_search/how_to/optimize_chat_prompts) for how to tune them.

Retrieve the current settings at any time:

<CodeGroup>
  ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  curl \
    -X GET 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
    -H "Authorization: Bearer MEILISEARCH_KEY"
  ```
</CodeGroup>

Update only the fields you want to change. Fields you omit remain unchanged:

<CodeGroup>
  ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
  curl \
    -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
    -H "Authorization: Bearer MEILISEARCH_KEY" \
    -H "Content-Type: application/json" \
    --data-binary '{ "apiKey": "your-valid-api-key" }'
  ```
</CodeGroup>

For the full HTTP parameter list, see:

* [Get settings of a chat workspace](/docs/reference/api/chats/get-settings-of-a-chat-workspace)
* [Update settings of a chat workspace](/docs/reference/api/chats/update-settings-of-a-chat-workspace)
* [Reset the settings of a chat workspace](/docs/reference/api/chats/reset-the-settings-of-a-chat-workspace)

#### LLM provider parameters passthrough

Meilisearch forwards standard chat completion parameters such as `temperature`, `top_p`, `frequency_penalty`, or `presence_penalty` to the configured provider. Available parameters depend on the provider. See the [chat completions API reference](/docs/reference/api/chats/request-a-chat-completion).

### Index chat settings

`chat` is an **index-level** setting, distinct from workspace settings. Configure it on every index the agent should access:

| Field                      | Type    | Default               | Description                                                                       |
| -------------------------- | ------- | --------------------- | --------------------------------------------------------------------------------- |
| `description`              | string  | `""`                  | Describes the index content so the LLM can decide when and how to query it        |
| `documentTemplate`         | string  | All searchable fields | Liquid template defining the text sent to the LLM for each document               |
| `documentTemplateMaxBytes` | integer | `400`                 | Maximum size in bytes of the rendered document template. Longer text is truncated |
| `searchParameters`         | object  | `{}`                  | Search parameters applied when the LLM queries this index                         |

Write `description` as if you were explaining the index to someone who has never seen your data. If you have multiple indexes, make each description specific enough that the LLM can distinguish them.

A good `documentTemplate` includes only the fields relevant to answering questions. See [document template best practices](/docs/capabilities/hybrid_search/advanced/document_template_best_practices).

`searchParameters` can enable hybrid search, limit results, or apply default sorting:

| Parameter               | Type      | Description                                                                                                       |
| ----------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
| `hybrid`                | object    | Enable hybrid search with `embedder` (required) and `semanticRatio` (0.0 for keyword only, 1.0 for semantic only) |
| `limit`                 | integer   | Maximum number of documents returned per search                                                                   |
| `sort`                  | string\[] | Sort order, for example `["price:asc", "rating:desc"]`                                                            |
| `distinct`              | string    | Return at most one document per distinct value of this attribute                                                  |
| `matchingStrategy`      | string    | How query terms are matched: `last`, `all`, or `frequency`                                                        |
| `attributesToSearchOn`  | string\[] | Restrict search to specific attributes                                                                            |
| `rankingScoreThreshold` | number    | Minimum ranking score (0.0 to 1.0) for a document to be included                                                  |

For conversational queries, a higher `semanticRatio` (0.6 to 0.8) and a lower `limit` (3 to 5) often work better than keyword-oriented defaults. See [optimize chat prompts](/docs/capabilities/agentic_search/how_to/optimize_chat_prompts).

For the full HTTP parameter list, see:

* [Get chat](/docs/reference/api/settings/get-chat)
* [Update chat](/docs/reference/api/settings/update-chat)
* [Reset chat](/docs/reference/api/settings/reset-chat)

## Streaming

Meilisearch uses [Server-Sent Events (SSE)](https://developer.mozilla.org/docs/Web/API/Server-sent_events) to stream chat completions. The wire format is OpenAI-compatible, so you can point the official OpenAI SDKs, the Vercel AI SDK, or any other SSE-aware client at the endpoint.

For the request itself, reuse the examples in [chat interface](#send-a-streaming-request). The `-N` flag in cURL disables output buffering so you see each chunk as it arrives.

### Understand the SSE response format

Each event on the wire follows three rules:

* Every event is a line that begins with the literal prefix `data: `
* The payload after `data: ` is a single JSON object shaped like an OpenAI `chat.completion.chunk`
* The stream terminates with the sentinel line `data: [DONE]`. The `[DONE]` marker is a literal string, not JSON, so parsers must check for it before calling `JSON.parse`

Events are separated by blank lines. After consuming `[DONE]`, close the reader.

#### Content chunks

Regular content chunks contain the AI-generated text in `choices[0].delta.content`:

<CodeGroup>
  ```
  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Meilisearch"},"finish_reason":null}]}

  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" is"},"finish_reason":null}]}
  ```
</CodeGroup>

#### Tool call chunks

When you include Meilisearch tools, the stream also contains tool call chunks in `choices[0].delta.tool_calls`:

<CodeGroup>
  ```
  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_abc123","type":"function","function":{"name":"_meiliSearchProgress","arguments":""}}]},"finish_reason":null}]}

  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"call_id\":\"abc\",\"function_name\":\"_meiliSearchInIndex\",\"function_parameters\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"search engine\\\"}\"}"}}]},"finish_reason":null}]}
  ```
</CodeGroup>

#### End of stream

The stream ends with a `finish_reason` of `"stop"` followed by the `[DONE]` marker:

<CodeGroup>
  ```
  data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

  data: [DONE]
  ```
</CodeGroup>

### Handle streaming in JavaScript

Use the Fetch API to process the SSE stream. Parse `data: ` lines, skip `[DONE]`, then handle `delta.content` and `delta.tool_calls`:

<CodeGroup>
  ```javascript JavaScript Fetch theme={null}
  async function streamChat(query) {
    const response = await fetch(
      'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions',
      {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer MEILISEARCH_KEY',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'gpt-4o',
          stream: true,
          messages: [{ role: 'user', content: query }],
          tools: MEILISEARCH_TOOLS, // see the complete example in Chat interface
        }),
      }
    );

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;

        const data = line.slice(6);
        if (data === '[DONE]') return;

        const chunk = JSON.parse(data);
        const delta = chunk.choices[0]?.delta;

        if (delta?.content) {
          process.stdout.write(delta.content);
        }

        if (delta?.tool_calls) {
          for (const toolCall of delta.tool_calls) {
            handleToolCall(toolCall);
          }
        }
      }
    }
  }
  ```
</CodeGroup>

The endpoint is stateless. Accumulate messages and send the full history with each request, including payloads from `_meiliAppendConversationMessage`. See the [complete example](#complete-example-progress-sources-and-history).

## Tools reference

Meilisearch intercepts three special tools and never forwards them to the LLM provider. Declare them in the `tools` array of your request.

<Warning>
  These tool definitions must include the exact parameter schemas below. Missing or incorrect parameters will prevent the tools from working.
</Warning>

Recommended usage order:

1. Handle progress updates with `_meiliSearchProgress`
2. Append conversation messages with `_meiliAppendConversationMessage`
3. Display source documents with `_meiliSearchSources`
4. Use `call_id` to associate progress updates with their corresponding source results

### `_meiliSearchProgress`

Reports real-time progress of internal search operations.

**Arguments:**

* `call_id`: Unique identifier to track the search operation
* `function_name`: Name of the internal function being executed (for example, `_meiliSearchInIndex`)
* `function_parameters`: JSON-encoded string containing search parameters such as `q` and `index_uid`

<CodeGroup>
  ```json _meiliSearchProgress theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliSearchProgress",
      "description": "Provides information about the current Meilisearch search operation",
      "parameters": {
        "type": "object",
        "properties": {
          "call_id": {
            "type": "string",
            "description": "The call ID to track the sources of the search"
          },
          "function_name": {
            "type": "string",
            "description": "The name of the function being executed"
          },
          "function_parameters": {
            "type": "string",
            "description": "The parameters of the function being executed, encoded in JSON"
          }
        },
        "required": ["call_id", "function_name", "function_parameters"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```
</CodeGroup>

### `_meiliAppendConversationMessage`

Asks the client to append internal tool calls and results to the conversation history.

**Arguments:**

* `role`: Message author role (`user` or `assistant`)
* `content`: Message content (for tool results)
* `tool_calls`: Array of tool calls made by the assistant
* `tool_call_id`: ID of the tool call this message responds to

<CodeGroup>
  ```json _meiliAppendConversationMessage theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliAppendConversationMessage",
      "description": "Append a new message to the conversation based on what happened internally",
      "parameters": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "description": "The role of the message author"
          },
          "content": {
            "type": "string",
            "description": "The content of the message. Required unless tool_calls is specified"
          },
          "tool_calls": {
            "type": ["array", "null"],
            "description": "Tool calls generated by the model",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "type": { "type": "string" },
                "function": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string" },
                    "arguments": { "type": "string" }
                  }
                }
              }
            }
          },
          "tool_call_id": {
            "type": ["string", "null"],
            "description": "Tool call this message is responding to"
          }
        },
        "required": ["role", "content", "tool_calls", "tool_call_id"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```
</CodeGroup>

### `_meiliSearchSources`

Returns the documents used by the LLM to generate the answer. The `call_id` matches `_meiliSearchProgress` so you can associate queries with results.

**Arguments:**

* `call_id`: Matches the `call_id` from `_meiliSearchProgress`
* `documents`: Source documents with only displayed attributes

<CodeGroup>
  ```json _meiliSearchSources theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliSearchSources",
      "description": "Provides sources of the search",
      "parameters": {
        "type": "object",
        "properties": {
          "call_id": {
            "type": "string",
            "description": "The call ID to track the original search associated to those sources"
          },
          "documents": {
            "type": "array",
            "items": { "type": "object" },
            "description": "The documents associated with the search. Only displayed attributes are returned"
          }
        },
        "required": ["call_id", "documents"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```
</CodeGroup>

See [display source documents](/docs/capabilities/agentic_search/how_to/display_source_documents) for UI patterns, and [display source documents with the Chats API](#display-source-documents-with-the-chats-api) for how to correlate the two tools by `call_id`. See the [complete example](#complete-example-progress-sources-and-history) for a request that declares all three tools.

## Display source documents with the Chats API

To [display source documents](/docs/capabilities/agentic_search/how_to/display_source_documents) using this API, declare the `_meiliSearchProgress` and `_meiliSearchSources` tools in your request (see [tools reference](#tools-reference) for their exact schemas), then correlate the two by the `call_id` they share as they stream in.

### Understand the response order

During a streamed response, tool calls arrive as chunks alongside content chunks, in this order:

1. `_meiliSearchProgress`: sent when the agent decides to search an index. Reports the query and index, and assigns a `call_id` to the search.
2. `_meiliSearchSources`: sent once the search completes, with the matching documents. Its `call_id` matches the progress event, so you can associate the documents with the query that produced them.
3. Content chunks: the AI-generated answer, based on the retrieved documents.

<CodeGroup>
  ```json _meiliSearchProgress theme={null}
  {
    "function": {
      "name": "_meiliSearchProgress",
      "arguments": "{\"call_id\":\"abc123\",\"function_name\":\"_meiliSearchInIndex\",\"function_parameters\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"best sci-fi movies\\\"}\"}"
    }
  }
  ```

  ```json _meiliSearchSources theme={null}
  {
    "function": {
      "name": "_meiliSearchSources",
      "arguments": "{\"call_id\":\"abc123\",\"documents\":[{\"id\":11,\"title\":\"Blade Runner 2049\"},{\"id\":27,\"title\":\"Interstellar\"}]}"
    }
  }
  ```
</CodeGroup>

Both events share the `call_id` value `abc123`, linking the "best sci-fi movies" search on the `movies` index to its two documents.

### Correlate sources by call\_id

Parse the `tool_calls` chunks from the [SSE stream](#understand-the-sse-response-format) and group them by `call_id` as they arrive:

<CodeGroup>
  ```javascript theme={null}
  const sources = new Map(); // call_id -> { query, index, documents }

  function handleToolCall(toolCall) {
    if (!toolCall.function?.name || !toolCall.function.arguments) return;

    const args = JSON.parse(toolCall.function.arguments);

    if (toolCall.function.name === '_meiliSearchProgress') {
      const params = JSON.parse(args.function_parameters);
      sources.set(args.call_id, {
        query: params.q,
        index: params.index_uid,
        documents: [],
      });
    }

    if (toolCall.function.name === '_meiliSearchSources') {
      const existing = sources.get(args.call_id);
      if (existing) {
        existing.documents = args.documents;
      }
    }
  }
  ```
</CodeGroup>

After the stream finishes, `sources` contains every search query and its documents, keyed by `call_id`. Pass this map to the UI patterns described in [display source documents](/docs/capabilities/agentic_search/how_to/display_source_documents#display-sources-in-your-ui).

## Handle errors and fallbacks with the Chats API

To [handle errors and fallbacks](/docs/capabilities/agentic_search/how_to/handle_errors_and_fallbacks) using this API, check the HTTP status before consuming the stream: Meilisearch forwards errors from your LLM provider on the chat completions endpoint. For generic fallback UX, user-facing messages, and AI SDK patterns, see the linked guide. This section covers what's specific to calling `/chats` directly.

### Check the response status before streaming

Wrap requests to the chat completions endpoint in error handling, and branch on the HTTP status before reading the response body. Once you start consuming the SSE stream, you can no longer branch on the status:

<CodeGroup>
  ```javascript theme={null}
  async function chat(messages) {
    try {
      const response = await fetch(
        `${MEILISEARCH_URL}/chats/${WORKSPACE}/chat/completions`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            model: MODEL,
            stream: true,
            messages,
            tools: MEILISEARCH_TOOLS, // see the tools reference
          })
        }
      );

      if (response.status === 429) {
        return {
          role: 'assistant',
          content: 'The service is currently experiencing high demand. Please try again in a moment.'
        };
      }

      if (response.status === 502 || response.status === 504) {
        return {
          role: 'assistant',
          content: 'The AI service is temporarily unavailable. Try a regular search instead.',
          fallback: true
        };
      }

      if (!response.ok) {
        const error = await response.json();
        console.error('Chat error:', error);
        return {
          role: 'assistant',
          content: 'Something went wrong. Please try rephrasing your question.'
        };
      }

      return response; // pass this to your SSE parser, see Streaming
    } catch (networkError) {
      return {
        role: 'assistant',
        content: 'Unable to connect to the search service. Please check your connection and try again.'
      };
    }
  }
  ```
</CodeGroup>

### Retry on rate limiting

The chat completions endpoint returns `429` when your LLM provider rate limits the request. Implement exponential backoff around the fetch call:

<CodeGroup>
  ```javascript theme={null}
  async function chatWithRetry(messages, maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      const response = await fetch(
        `${MEILISEARCH_URL}/chats/${WORKSPACE}/chat/completions`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ model: MODEL, stream: true, messages, tools: MEILISEARCH_TOOLS })
        }
      );

      if (response.status !== 429) {
        return response;
      }

      // Exponential backoff: 1s, 2s, 4s
      const waitMs = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, waitMs));
    }

    return {
      fallback: true,
      content: 'The service is busy. Please try again shortly.'
    };
  }
  ```
</CodeGroup>

### Fall back to regular search

When conversational search fails, fall back to a standard keyword or hybrid search against `/indexes/{index_uid}/search`:

<CodeGroup>
  ```javascript theme={null}
  async function searchWithFallback(query, conversationHistory) {
    const chatResponse = await chat([
      ...conversationHistory,
      { role: 'user', content: query }
    ]);

    if (chatResponse.fallback) {
      const searchResponse = await fetch(
        `${MEILISEARCH_URL}/indexes/${INDEX}/search`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            q: query,
            hybrid: { semanticRatio: 0.5, embedder: EMBEDDER }
          })
        }
      );

      const results = await searchResponse.json();
      return {
        type: 'search',
        hits: results.hits,
        message: 'Showing search results instead. The AI assistant is temporarily unavailable.'
      };
    }

    return { type: 'chat', response: chatResponse };
  }
  ```
</CodeGroup>

### Detect empty results from `_meiliSearchSources`

Unlike a regular tool result, `_meiliSearchSources` arrives as a tool call chunk in the stream (see [tools reference](#tools-reference) for its schema). Parse its `documents` argument and, if it is empty, show a fallback message instead of relying on the model's answer:

<CodeGroup>
  ```javascript theme={null}
  function handleSources(toolCall) {
    const args = JSON.parse(toolCall.function.arguments);

    if (!Array.isArray(args.documents) || args.documents.length === 0) {
      showMessage('No matching documents found. Try different keywords or broaden your search.');
      return;
    }

    displaySources(args.documents);
  }
  ```
</CodeGroup>

Combine this with [guardrails](/docs/capabilities/agentic_search/how_to/configure_guardrails) in your workspace's system prompt, so the model itself also acknowledges when it found nothing relevant.

## Troubleshooting

### Missing default chat API key

If your instance does not have a Default Chat API Key, [create one manually](#find-your-chat-api-key).

### Empty reply from server (curl error 52)

**Causes:**

* Chat completions feature not enabled
* Missing authentication in requests

**Solution:**

1. [Enable the feature](#enable-the-chat-completions-feature)
2. Include the `Authorization` header in all requests

### "Invalid API key" error

**Cause:** Using the wrong type of API key

**Solution:**

* Use the "Default Chat API Key"
* Do not use search or admin API keys for chat endpoints
* Find your chat key with the [list keys endpoint](/docs/reference/api/keys/list-api-keys)

### "Socket connection closed unexpectedly"

**Cause:** Usually means the LLM provider API key is missing or invalid in workspace settings

**Solution:**

1. Check workspace configuration:

   <CodeGroup>
     ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
     curl \
       -X GET 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
       -H "Authorization: Bearer MEILISEARCH_KEY"
     ```
   </CodeGroup>

2. Update with a valid API key:

   <CodeGroup>
     ```bash cURL theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null} theme={null}
     curl \
       -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
       -H "Authorization: Bearer MEILISEARCH_KEY" \
       -H "Content-Type: application/json" \
       --data-binary '{ "apiKey": "your-valid-api-key" }'
     ```
   </CodeGroup>

### No search progress visible

**Cause:** The `_meiliSearchProgress` tool is not declared in the request

**Solution:**

The search still runs and the LLM still answers, but without `_meiliSearchProgress` you receive no visibility into what searches are being performed. Add all three Meilisearch tools as shown in the [complete example](#complete-example-progress-sources-and-history).

For HTTP status handling, rate limits, empty results, and falling back to regular search when calling `/chats` directly, see [handle errors and fallbacks with the Chats API](#handle-errors-and-fallbacks-with-the-chats-api). For generic fallback UX and AI SDK patterns, see [handle errors and fallbacks](/docs/capabilities/agentic_search/how_to/handle_errors_and_fallbacks).

## Next steps

<CardGroup cols={2}>
  <Card title="Agentic search getting started" href="/docs/capabilities/agentic_search/getting_started">
    Build agentic and conversational search with the recommended AI SDK.
  </Card>

  <Card title="Display source documents" href="/docs/capabilities/agentic_search/how_to/display_source_documents">
    Show users which documents were used to generate responses.
  </Card>

  <Card title="Configure guardrails" href="/docs/capabilities/agentic_search/how_to/configure_guardrails">
    Restrict AI responses to topics covered by your data.
  </Card>

  <Card title="Optimize chat prompts" href="/docs/capabilities/agentic_search/how_to/optimize_chat_prompts">
    Tune system prompts, tool prompts, and index chat settings.
  </Card>

  <Card title="Reduce hallucination" href="/docs/capabilities/agentic_search/advanced/reduce_hallucination">
    Techniques to keep AI responses grounded in your data.
  </Card>

  <Card title="Chat completions API reference" href="/docs/reference/api/chats/request-a-chat-completion">
    Full reference for the chat completions endpoint.
  </Card>
</CardGroup>
