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

# Agent - Query

> Send messages to an AI agent and receive responses. Supports text queries, file attachments, real-time streaming, and conversation continuity.

This endpoint allows you to send messages to an AI agent and receive responses. It supports:

* Simple text queries
* File attachments (documents, images, audio)
* Real-time response streaming
* Continuation of existing conversations
* Contact association (CRM)
* Per-request overrides of the agent's prompt, model and reasoning level

### Path

<ParamField path="agentId" type="string" required>
  The ID of the agent you want to query (CUID format).
</ParamField>

### Body

#### Required

<ParamField body="query" type="string" required>
  The user's message or question.
</ParamField>

#### Optional - Basic Configuration

<ParamField body="streaming" type="boolean" default="false">
  If `true`, responds with Server-Sent Events in real-time.
</ParamField>

<ParamField body="conversationId" type="string">
  ID to continue an existing conversation. Auto-generated if not provided.
</ParamField>

<ParamField body="visitorId" type="string">
  Unique ID of the visitor/user. Auto-generated if not provided.
</ParamField>

<ParamField body="channel" type="string" default="api">
  Source channel for the message. Valid values: `api`, `dashboard`, `website`, `form`, `whatsapp`, `telegram`, `slack`, `meta`, `crisp`, `zapier`, `mail`, `mercadolibre`, `agent_builder`, `chatwoot`, `crmchatsappai`.
</ParamField>

<ParamField body="context" type="string">
  Additional context for the AI (e.g., specific instructions).
</ParamField>

<ParamField body="systemPrompt" type="string">
  Overrides the agent's system prompt **for this request only**. The agent's saved configuration in the database is not modified.

  Use this to test different prompt variants, run evaluations, or inject custom instructions without editing the agent from the dashboard.

  <Note>
    An empty string `""` is treated as no override — the agent's saved system prompt is used instead. If both `systemPrompt` and `promptType: "raw"` are provided, `promptType: "raw"` takes precedence.
  </Note>
</ParamField>

#### Optional - Model & Reasoning Overrides

<ParamField body="modelName" type="string">
  Overrides the agent's model **for this request only**. The agent's saved configuration in the database is not modified.

  Use the identifier form used by the agent configuration, with underscores — for example `gpt_5_6_sol`, `gemini_3_8_flash`, `claude_5_sonnet`, `gpt_6_luna`, `glm_5_3`.

  <Note>
    These identifiers are not the same as the provider-facing model names used by the Increment Agent Queries endpoint. Here it is `gpt_5_6_sol`, there it is `gpt-5.6-sol`.
  </Note>
</ParamField>

<ParamField body="reasoningEffort" type="string">
  Controls how much internal reasoning the model does before answering, **for this request only**. The agent's saved reasoning level is not modified.

  Valid values: `off`, `low`, `medium`, `high`, `max`. Always send one of these five — Laburen translates them to whatever each provider expects, so you never send provider-native values such as `none` or a thinking budget.

  Which of the five a model accepts depends on the model. Sending a level the model does not support returns `400`.

  | `modelName` | Accepted values | Default |
  | - | - | - |
  | `gpt_5_6_sol` | `off`, `low`, `medium`, `high` | `high` |
  | `gpt_5_6_terra` | `off`, `low`, `medium`, `high` | `high` |
  | `gpt_5_6_luna` | `off`, `low`, `medium`, `high` | `medium` |
  | `glm_5_2` | `off`, `low`, `medium`, `high`, `max` | `high` |
  | `glm_5_3` | `low`, `medium`, `high`, `max` | `medium` |
  | `glm_5_3_flash` | `low`, `medium`, `high`, `max` | `medium` |
  | `claude_5_sonnet` | `off`, `low`, `medium`, `high`, `max` | `medium` |
  | `claude_5_5_opus` | `low`, `medium`, `high`, `max` | `medium` |
  | `gpt_6_astra` | `low`, `medium`, `high`, `max` | `medium` |
  | `gpt_6_sol` | `off`, `low`, `medium`, `high`, `max` | `medium` |
  | `gpt_6_luna` | `off`, `low`, `medium`, `high`, `max` | `low` |
  | `gemini_3_pro_preview` | `low`, `medium`, `high` | `high` |
  | `gemini_3_1_pro_preview` | `low`, `medium`, `high` | `high` |
  | `gemini_3_flash` | `low`, `medium`, `high` | `high` |
  | `gemini_3_8_flash` | `low`, `medium`, `high` | `medium` |
  | `gemini_3_5_flash_lite` | `off`, `low`, `medium`, `high` | `off` |

  The exceptions worth remembering: most Gemini models cannot turn reasoning off — `gemini_3_5_flash_lite` is the only one that accepts `off`, and there `off` means Google's lowest level rather than reasoning fully disabled. `glm_5_3`, `glm_5_3_flash`, `claude_5_5_opus` and `gpt_6_astra` cannot turn it off either: reasoning is mandatory on those models. The DeepSeek models (`deepseek_r1`, `deepseek_v4_pro`, `deepseek_v4_flash`) always reason at a fixed level, so they reject every value.

  <Note>
    On `gemini_3_8_flash`, `low` produces little or no reasoning in practice, even on complex prompts. If you need the model to actually reason, start at `medium`.
  </Note>

  <Warning>
    Any model not listed in the table has no configurable reasoning. Sending `reasoningEffort` to one of those returns `400`.
  </Warning>

  The level is validated against the model that will actually run the request: the `modelName` you send in this same body, or the agent's saved model when you omit it.

  When you omit `reasoningEffort`, the agent's saved level is used. If the agent has none saved, the model's default from the table applies. The override works with conversational mode on or off.
</ParamField>

#### Optional - Webhook (Conversational Mode)

<ParamField body="webhookUrl" type="string">
  URL to receive the AI response asynchronously. **Required when the agent has conversational mode enabled and `channel` is `api`**.

  The webhook URL must:

  * Be a valid URL format
  * Use `http://` or `https://` protocol
  * Be reachable (the server validates connectivity)
  * Not point to localhost or private IPs in production (SSRF protection)
</ParamField>

#### Optional - File Attachments

<ParamField body="attachments" type="array">
  List of file attachments.

  <Expandable title="Attachment object">
    <ParamField body="url" type="string" required>
      URL of the file to attach.
    </ParamField>
  </Expandable>
</ParamField>

#### Optional - Contact (CRM)

<ParamField body="contact" type="object">
  Contact data to associate with the conversation.

  <Expandable title="Contact object">
    <ParamField body="email" type="string">
      Contact's email address.
    </ParamField>

    <ParamField body="phoneNumber" type="string">
      Contact's phone number.
    </ParamField>

    <ParamField body="firstName" type="string">
      Contact's first name.
    </ParamField>

    <ParamField body="lastName" type="string">
      Contact's last name.
    </ParamField>

    <ParamField body="userId" type="string">
      External user ID.
    </ParamField>
  </Expandable>
</ParamField>

### Response (without streaming)

<ResponseField name="answer" type="string">
  The agent's response.
</ResponseField>

<ResponseField name="sources" type="array">
  Sources used to generate the response.

  <Expandable title="Source object">
    <ResponseField name="source" type="string">
      Name of the source document.
    </ResponseField>

    <ResponseField name="chunk" type="string">
      Relevant fragment from the source.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="messageId" type="string">
  ID of the response message.
</ResponseField>

<ResponseField name="conversationId" type="string">
  ID of the conversation (save this to continue the conversation).
</ResponseField>

<ResponseField name="visitorId" type="string">
  ID of the visitor.
</ResponseField>

<ResponseField name="request_human" type="boolean">
  Whether the agent requested human intervention.
</ResponseField>

<ResponseField name="status" type="string">
  Conversation status (e.g., `UNRESOLVED`, `RESOLVED`).
</ResponseField>

<ResponseField name="usage" type="object">
  Token usage and cost information.

  <Expandable title="Usage object" defaultOpen>
    <ResponseField name="completionTokens" type="integer">
      Number of tokens in the completion/response.
    </ResponseField>

    <ResponseField name="promptTokens" type="integer">
      Number of tokens in the prompt/context.
    </ResponseField>

    <ResponseField name="totalTokens" type="integer">
      Total tokens used (prompt + completion).
    </ResponseField>

    <ResponseField name="cost" type="number">
      Estimated cost of the request.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="approvals" type="array">
  List of approvals for tool executions (if any).
</ResponseField>

### Response (Conversational Mode with Webhook)

When the agent has **conversational mode enabled** and you provide a `webhookUrl`, the endpoint returns immediately with a queued status:

<ResponseField name="status" type="string">
  Always `"queued"` for webhook mode.
</ResponseField>

<ResponseField name="conversationId" type="string">
  ID of the conversation.
</ResponseField>

<ResponseField name="visitorId" type="string">
  ID of the visitor.
</ResponseField>

<ResponseField name="inputMessageId" type="string">
  ID of the user's input message.
</ResponseField>

<ResponseField name="message" type="string">
  Confirmation message indicating the request was queued.
</ResponseField>

<ResponseField name="webhookUrl" type="string">
  The webhook URL where the response will be sent.
</ResponseField>

#### Webhook Payload

After processing (typically 8 seconds delay to batch multiple messages), a POST request is sent to your `webhookUrl` with the following payload:

```json theme={null}
{
  "conversationId": "clxxxxxxxxxxxxxxxxx",
  "visitorId": "visitor-abc123",
  "status": "success",
  "messages": [
    {
      "id": "msg_001",
      "text": "First part of the response...",
      "createdAt": "2024-01-15T10:30:00.000Z"
    },
    {
      "id": "msg_002",
      "text": "Second part of the response...",
      "createdAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "agentResponse": {
    "answer": "Complete response without splitting...",
    "sources": [],
    "usage": {
      "completionTokens": 150,
      "promptTokens": 500,
      "totalTokens": 650,
      "cost": 0.0065
    },
    "metadata": {}
  },
  "messageId": "msg_answer456"
}
```

**Webhook Headers:**

* `Content-Type: application/json`
* `X-Laburen-Event: agent.response`

<Note>
  In conversational mode, long responses are automatically split into multiple messages (maximum 3 for most channels, up to 10 for Instagram/Meta).
</Note>

### Streaming Response

When `streaming: true`, the endpoint responds with Server-Sent Events:

```
Content-Type: text/event-stream

event: answer
data: Hello,

event: answer
data:  how can

event: answer
data:  I help you?

event: endpoint_response
data: {"messageId":"...","answer":"...","conversationId":"..."}

data: [DONE]
```

**Events:**

* `answer`: Partial response text (concatenate to build the full answer)
* `endpoint_response`: Full response object (JSON) with all metadata

### Error Responses

| Status Code | Type | Description |
| - | - | - |
| 400 | Bad Request | Invalid channel value. Returns list of valid channels. |
| 400 | Bad Request | Missing `webhookUrl` when agent has conversational mode enabled and `channel` is `api`. |
| 400 | Bad Request | Invalid `webhookUrl` (malformed URL, invalid protocol, unreachable server). |
| 400 | Bad Request | Unknown `reasoningEffort` value. Returns the list of valid levels. |
| 400 | Bad Request | `reasoningEffort` not supported by the model that will run the request. |
| 401 | UNAUTHORIZED | Invalid API Key or insufficient permissions |
| 404 | NOT\_FOUND | Agent not found |
| 500 | Internal Error | Error processing the message |

<RequestExample>
  ```bash cURL theme={null}
  curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/query' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API_KEY>' \
  --data-raw '{
      "query": "Hello, I need help with my order",
      "conversationId": "clxxxxxxxxxxxxxxxxx",
      "visitorId": "clxxxxxxxxxxxxxxxxx"
  }'
  ```

  ```javascript JavaScript theme={null}
  const apiUrl = 'https://dashboard.laburen.com/api';
  const apiKey = '<API_KEY>';
  const agentId = '<agentId>';

  const response = await fetch(`${apiUrl}/agents/${agentId}/query`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      query: 'Hello, I need help with my order',
      conversationId: 'clxxxxxxxxxxxxxxxxx', // Optional: to continue conversation
      visitorId: 'clxxxxxxxxxxxxxxxxx',       // Optional: user identifier
      // systemPrompt: 'You are a helpful assistant that only answers in English.', // Optional override
      // modelName: 'gpt_5_6_luna',       // Optional: run this request on another model
      // reasoningEffort: 'high',         // Optional: off | low | medium | high | max
    }),
  });

  const data = await response.json();
  console.log(data.answer);
  // Save data.conversationId to continue the conversation
  ```

  ```python Python theme={null}
  import requests

  api_url = "https://dashboard.laburen.com/api"
  api_key = "<API_KEY>"
  agent_id = "<agentId>"

  response = requests.post(
      f"{api_url}/agents/{agent_id}/query",
      headers={
          "Content-Type": "application/json",
          "Authorization": f"Bearer {api_key}",
      },
      json={
          "query": "Hello, I need help with my order",
          "conversationId": "clxxxxxxxxxxxxxxxxx",   # Optional: to continue conversation
          "visitorId": "clxxxxxxxxxxxxxxxxx",        # Optional: user identifier
          # "systemPrompt": "You are a helpful assistant.",  # Optional override
          # "modelName": "gpt_5_6_luna",                    # Optional: run on another model
          # "reasoningEffort": "high",                      # Optional: off | low | medium | high | max
      },
  )

  data = response.json()
  print(data["answer"])
  # Save data["conversationId"] to continue the conversation
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "answer": "Hello! I'd be happy to help you with your order. Could you please provide your order number so I can look it up?",
    "usage": {
      "completionTokens": 28,
      "promptTokens": 1250,
      "totalTokens": 1278,
      "cost": 0.0064
    },
    "sources": [
      {
        "source": "FAQ.pdf",
        "chunk": "To check your order status, please provide your order number..."
      }
    ],
    "approvals": [],
    "messageId": "clxxxxxxxxxxxxxxxxx",
    "conversationId": "clxxxxxxxxxxxxxxxxx",
    "visitorId": "clxxxxxxxxxxxxxxxxx",
    "request_human": false,
    "status": "UNRESOLVED"
  }
  ```
</ResponseExample>

### Model and Reasoning Override Example

Run a single request on a different model and reasoning level without changing the agent. Useful for A/B testing prompts, evaluations, or spending more reasoning on a hard question while the agent stays cheap by default.

```bash cURL theme={null}
curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
    "query": "Compare these two contracts and tell me which one is riskier",
    "modelName": "gpt_5_6_sol",
    "reasoningEffort": "high"
}'
```

To make the same agent answer as fast and cheap as possible, send the opposite:

```bash cURL theme={null}
curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
    "query": "What are your opening hours?",
    "modelName": "gpt_5_6_sol",
    "reasoningEffort": "off"
}'
```

If the model rejects the level, the response is a `400`:

```json theme={null}
{
  "error": "Model \"gemini_3_flash\" does not support reasoningEffort \"off\""
}
```

<Note>
  Neither override is persisted. The agent keeps the model and reasoning level saved from the dashboard, so the next request without these fields behaves exactly as before.
</Note>

### Streaming Example (JavaScript)

```javascript theme={null}
import {
  EventStreamContentType,
  fetchEventSource,
} from '@microsoft/fetch-event-source';

const apiUrl = 'https://dashboard.laburen.com/api';
const apiKey = '<API_KEY>';
const agentId = '<agentId>';

let answer = '';
let endpointResponse = '';
const ctrl = new AbortController();

await fetchEventSource(`${apiUrl}/agents/${agentId}/query`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
  },
  signal: ctrl.signal,
  body: JSON.stringify({
    streaming: true,
    query: 'Hello, I need help',
    conversationId: 'optional-conversation-id',
    visitorId: 'optional-visitor-id',
  }),

  async onopen(response) {
    if (response.status === 401) {
      throw new Error('Unauthorized');
    }
    if (response.status === 402) {
      throw new Error('Usage limit exceeded');
    }
  },

  onmessage: (event) => {
    if (event.data === '[DONE]') {
      // End of stream
      ctrl.abort();

      // Parse the full response
      const fullResponse = JSON.parse(endpointResponse);
      console.log('Full response:', fullResponse);
    } else if (event.data?.startsWith('[ERROR]')) {
      console.error('Stream error:', event.data);
    } else if (event.event === 'endpoint_response') {
      endpointResponse += event.data;
    } else if (event.event === 'answer') {
      answer += event.data;
      // Update UI with partial answer
      console.log('Partial answer:', answer);
    }
  },

  onerror: (error) => {
    console.error('Connection error:', error);
  },
});
```

### Webhook Example (Conversational Mode)

When your agent has conversational mode enabled, use `webhookUrl` to receive the AI response asynchronously:

```bash cURL theme={null}
curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
    "query": "Hello, I need help",
    "channel": "api",
    "webhookUrl": "https://your-server.com/webhook/laburen"
}'
```

**Immediate Response:**

```json theme={null}
{
  "status": "queued",
  "conversationId": "clxxxxxxxxxxxxxxxxx",
  "visitorId": "visitor-abc123",
  "inputMessageId": "msg_input123",
  "message": "Your message has been queued. Response will be sent to the provided webhookUrl after processing.",
  "webhookUrl": "https://your-server.com/webhook/laburen"
}
```

**Webhook Receiver Example (Node.js/Express):**

```javascript theme={null}
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook/laburen', (req, res) => {
  const event = req.headers['x-laburen-event'];

  if (event === 'agent.response') {
    const { conversationId, messages, agentResponse } = req.body;

    console.log('Conversation:', conversationId);
    console.log('Messages:', messages.length);

    // Process each split message
    messages.forEach((msg, i) => {
      console.log(`Message ${i + 1}:`, msg.text);
    });

    // Full answer available in agentResponse
    console.log('Full answer:', agentResponse.answer);
    console.log('Usage:', agentResponse.usage);
  }

  res.status(200).send('OK');
});

app.listen(4000, () => {
  console.log('Webhook server listening on port 4000');
});
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.