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

# Stateless converse

> Stream one model turn with your own history and your own tools, with no conversation stored on the platform.

`POST /api/v1/converse` streams one model turn over Server-Sent Events. You send the whole conversation every time, you supply the tool definitions, and you run any tool the model asks for. The platform stores no conversation and runs no tool. Use it when your application already owns conversation state and you want a metered model gateway. Use a [Conversation instance](/conversations/overview) when you want the platform to keep transcripts, run platform tools, enforce guardrails, and gate releases with evals.

Credential: an organization API key, or a scoped key with `conversations:use`. The route uses the default org rate limit bucket and is exempt from the 30-second request timeout.

## How it works

1. You post `messages` (the whole history), optional `system`, optional `tools`, and optional `model` and `max_tokens`.
2. The platform streams `message_start`, then `text_delta` frames and one `tool_use` frame per tool call the model makes, then `message_end`.
3. If `message_end.stop_reason` is `tool_use`, you run the tools, append the assistant's `tool_use` blocks and your `tool_result` blocks to `messages`, and post the whole conversation again.

## Request

| Field        | Type    | Required | Rules                                                                                                                                                                                                                      |
| ------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages`   | array   | Yes      | 1 to 100 messages. Each has `role` (`user` or `assistant`) and a non-empty `content` array.                                                                                                                                |
| `system`     | string  | No       | System prompt.                                                                                                                                                                                                             |
| `tools`      | array   | No       | At most 64. Each has `name`, optional `description`, and a required `input_schema` (a JSON Schema).                                                                                                                        |
| `max_tokens` | integer | No       | 0 to 8192. Omitted or `0` means 1024.                                                                                                                                                                                      |
| `model`      | string  | No       | A model id from the platform's model catalog. Omitted means the platform default, which is `gpt-6-sol` on deployments that use OpenAI. An id that is unknown, or whose provider is not configured, is `400 unknown_model`. |

Content blocks:

| `type`        | Fields                                          | Use                                      |
| ------------- | ----------------------------------------------- | ---------------------------------------- |
| `text`        | `text`                                          | Prose from the user or the assistant.    |
| `tool_use`    | `id`, `name`, `input`                           | Replays a tool call the model made.      |
| `tool_result` | `tool_use_id`, `content` (any JSON), `is_error` | Your tool's output, in a `user` message. |

File and image blocks are not accepted on this route.

The example below uses Harbor Group, a fictional company whose staff look up the review status of its suppliers. The tool runs in your application, not on the platform. Tool names use letters, digits, `_`, `.`, and `-`, must not contain `__`, and must be at most 64 characters where each `.` counts as two.

<CodeGroup>
  ```bash curl theme={null}
  curl -N -X POST https://api.usenexio.com/api/v1/converse \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Accept: text/event-stream" \
    -d '{
      "system": "You help staff at Harbor Group look up supplier records.",
      "max_tokens": 512,
      "messages": [
        {"role": "user", "content": [{"type": "text", "text": "Is supplier SUP-2026-004417 still approved?"}]}
      ],
      "tools": [
        {
          "name": "lookup_supplier_status",
          "description": "Read the current review status of a supplier by supplier id.",
          "input_schema": {
            "type": "object",
            "properties": {"supplier_id": {"type": "string"}},
            "required": ["supplier_id"]
          }
        }
      ]
    }'
  ```

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

  body = {
      "system": "You help staff at Harbor Group look up supplier records.",
      "max_tokens": 512,
      "messages": [
          {"role": "user", "content": [{"type": "text", "text": "Is supplier SUP-2026-004417 still approved?"}]}
      ],
      "tools": [
          {
              "name": "lookup_supplier_status",
              "description": "Read the current review status of a supplier by supplier id.",
              "input_schema": {
                  "type": "object",
                  "properties": {"supplier_id": {"type": "string"}},
                  "required": ["supplier_id"],
              },
          }
      ],
  }

  with requests.post(
      "https://api.usenexio.com/api/v1/converse",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"], "Accept": "text/event-stream"},
      json=body,
      stream=True,
      timeout=(10, 300),
  ) as resp:
      if resp.status_code != 200:
          raise RuntimeError(resp.json())
      event = None
      for line in resp.iter_lines(decode_unicode=True):
          if line.startswith("event: "):
              event = line[7:]
          elif line.startswith("data: "):
              print(event, json.loads(line[6:]))
  ```

  ```typescript TypeScript theme={null}
  const body = {
    system: "You help staff at Harbor Group look up supplier records.",
    max_tokens: 512,
    messages: [
      { role: "user", content: [{ type: "text", text: "Is supplier SUP-2026-004417 still approved?" }] },
    ],
    tools: [
      {
        name: "lookup_supplier_status",
        description: "Read the current review status of a supplier by supplier id.",
        input_schema: {
          type: "object",
          properties: { supplier_id: { type: "string" } },
          required: ["supplier_id"],
        },
      },
    ],
  }

  const resp = await fetch("https://api.usenexio.com/api/v1/converse", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "text/event-stream",
    },
    body: JSON.stringify(body),
  })
  if (resp.status !== 200) throw new Error(JSON.stringify(await resp.json()))
  console.log(await resp.text())
  ```
</CodeGroup>

## The stream

Headers and framing match the instance turn route: `Content-Type: text/event-stream`, one `event:` line and one `data:` line of JSON per frame, then a blank line.

| Frame           | Payload                      | When                                               |
| --------------- | ---------------------------- | -------------------------------------------------- |
| `message_start` | `{type}`                     | First frame.                                       |
| `text_delta`    | `{type, text}`               | A piece of assistant prose.                        |
| `tool_use`      | `{type, id, name, input}`    | One complete tool call. Never split across frames. |
| `message_end`   | `{type, stop_reason, usage}` | Terminal success frame.                            |
| `error`         | `{type, code, message}`      | Terminal failure frame after the stream started.   |

```text theme={null}
event: message_start
data: {"type":"message_start"}

event: text_delta
data: {"type":"text_delta","text":"Let me check that supplier."}

event: tool_use
data: {"type":"tool_use","id":"call_Hd72KqPz0sVm","name":"lookup_supplier_status","input":{"supplier_id":"SUP-2026-004417"}}

event: message_end
data: {"type":"message_end","stop_reason":"tool_use","usage":{"input_tokens":212,"output_tokens":38,"total_tokens":250,"cached_input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}
```

`stop_reason` is `end_turn` for a finished answer, `tool_use` when the model wants tools run, and `max_tokens` when the output was cut off at `max_tokens`. Any other provider value passes through unchanged. `usage` has the same six fields as on instance turns; `cached_input_tokens` is filled by OpenAI models and is a subset of `input_tokens`, while `cache_creation_input_tokens` and `cache_read_input_tokens` are filled by Anthropic models.

## Continue after a tool call

Append the assistant's tool call and your result, then post everything again:

```json theme={null}
{
  "system": "You help staff at Harbor Group look up supplier records.",
  "max_tokens": 512,
  "messages": [
    {"role": "user", "content": [{"type": "text", "text": "Is supplier SUP-2026-004417 still approved?"}]},
    {"role": "assistant", "content": [
      {"type": "text", "text": "Let me check that supplier."},
      {"type": "tool_use", "id": "call_Hd72KqPz0sVm", "name": "lookup_supplier_status", "input": {"supplier_id": "SUP-2026-004417"}}
    ]},
    {"role": "user", "content": [
      {"type": "tool_result", "tool_use_id": "call_Hd72KqPz0sVm", "content": {"status": "approved", "review_due_on": "2026-11-01"}}
    ]}
  ],
  "tools": [
    {
      "name": "lookup_supplier_status",
      "description": "Read the current review status of a supplier by supplier id.",
      "input_schema": {"type": "object", "properties": {"supplier_id": {"type": "string"}}, "required": ["supplier_id"]}
    }
  ]
}
```

## Errors

Before the first frame, errors use the JSON envelope. After it, the status is already `200` and the failure is a terminal `error` frame with `code` and `message`. This route does not include the provider failure `reason` that instance turns carry.

| Status | Code                                      | Cause                                                                                             |
| ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`                         | Body is not JSON or is larger than 1 MiB, or `messages` is empty.                                 |
| 400    | `too_many_messages`                       | More than 100 messages.                                                                           |
| 400    | `too_many_tools`                          | More than 64 tools.                                                                               |
| 400    | `invalid_max_tokens`                      | `max_tokens` below 0 or above 8192.                                                               |
| 400    | `invalid_message_role`                    | A role other than `user` or `assistant`.                                                          |
| 400    | `invalid_message`                         | A message with empty `content`.                                                                   |
| 400    | `invalid_content_block`                   | A block type other than `text`, `tool_use`, `tool_result`.                                        |
| 400    | `invalid_tool`                            | Empty name, `__` in a name, a disallowed character, a name too long, or a missing `input_schema`. |
| 400    | `unknown_model`                           | `model` is not in the catalog or its provider is not configured.                                  |
| 401    | `unauthorized`                            | Missing or invalid key.                                                                           |
| 403    | `insufficient_capability`                 | Scoped key without `conversations:use`.                                                           |
| 429    | `rate_limited`                            | Org rate limit reached. See `Retry-After`.                                                        |
| 500    | `internal_error`, `streaming_unsupported` | Platform fault.                                                                                   |
| 502    | `provider_error`                          | The model provider failed.                                                                        |
| 503    | `provider_unavailable`                    | The provider circuit is open after repeated failures. Retry later.                                |
| 503    | `converse_unavailable`                    | Converse is not enabled in this deployment.                                                       |

A model call that fails transiently is attempted up to three times, with waits of at most 5 seconds, while nothing has been streamed. Once a frame is sent, a failure is final.

## Metering

A completed call records one usage row for your org with the model and token counts, when the provider reports token usage. Stateless calls are not tied to any instance or conversation.

<CardGroup cols={2}>
  <Card title="Conversations overview" href="/conversations/overview">
    When a managed instance fits better.
  </Card>

  <Card title="Converse (API reference)" href="/api-reference/conversations/converse/converse">
    The generated endpoint contract.
  </Card>
</CardGroup>
