> ## 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.

# Your first conversation

> Create a conversation, stream one turn, and read the stored transcript back.

This guide is for a developer wiring a server-side integration. You create a conversation for one end user, send a message, read the streamed answer, and fetch the stored transcript. It takes four requests.

## Prerequisites

* An API key. Either an organization API key from the portal (**Settings**, then **API keys**) or a scoped key (`nxsk_v1_...`) that carries `conversations:use`. The examples read the key from the `NEXIO_API_KEY` environment variable. See [Authentication and access](/authentication).
* A Conversation instance with at least one published version. Create and publish one in the portal under **Conversations**, or see [Versions and publish](/conversations/versions-and-publish). The examples use the slug `workspace-assistant`.
* An `end_user` value for the person your application is serving. The examples use `u_dana_ortiz`. Send the same value on every request for that person's conversations.

Call these routes from your server. The API sends no CORS headers, and the key must never reach a browser.

<Steps>
  <Step title="Confirm the instance is published">
    List the instance's released versions. For an instance you manage yourself, an empty list means turns will return `409 instance_not_published`. An instance that follows another instance (a follower) runs the versions released on the instance it follows, so its own list can be empty while turns work.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/versions \
        -H "Authorization: Bearer $NEXIO_API_KEY"
      ```

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

      API = "https://api.usenexio.com/api/v1"
      HEADERS = {"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"]}

      resp = requests.get(f"{API}/conversation-instances/workspace-assistant/versions", headers=HEADERS)
      resp.raise_for_status()
      print(resp.json())
      ```

      ```typescript TypeScript theme={null}
      const API = "https://api.usenexio.com/api/v1"
      const HEADERS = { Authorization: `Bearer ${process.env.NEXIO_API_KEY}` }

      const resp = await fetch(`${API}/conversation-instances/workspace-assistant/versions`, { headers: HEADERS })
      if (!resp.ok) throw new Error(`versions: ${resp.status}`)
      console.log(await resp.json())
      ```
    </CodeGroup>

    Response `200`:

    ```json theme={null}
    {
      "versions": [
        {
          "version": 1,
          "config_hash": "4f1c9a7e2b8d3065",
          "changelog": "First release",
          "created_by": "user_01J8Q4Z7M2R6T9V3X5B1N0K8HD",
          "released_at": "2026-09-22T16:40:05Z"
        }
      ]
    }
    ```
  </Step>

  <Step title="Create a conversation">
    Create a conversation for the end user. `title` and `scope` are optional. `scope` is a JSON object your application can use to remember what the conversation is about. It never grants access to anything.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations \
        -H "Authorization: Bearer $NEXIO_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "end_user": "u_dana_ortiz",
          "title": "Getting started"
        }'
      ```

      ```python Python theme={null}
      resp = requests.post(
          f"{API}/conversation-instances/workspace-assistant/conversations",
          headers=HEADERS,
          json={"end_user": "u_dana_ortiz", "title": "Getting started"},
      )
      resp.raise_for_status()
      conversation = resp.json()
      conversation_id = conversation["id"]
      ```

      ```typescript TypeScript theme={null}
      const created = await fetch(`${API}/conversation-instances/workspace-assistant/conversations`, {
        method: "POST",
        headers: { ...HEADERS, "Content-Type": "application/json" },
        body: JSON.stringify({ end_user: "u_dana_ortiz", title: "Getting started" }),
      })
      if (!created.ok) throw new Error(`create: ${created.status}`)
      const conversation = await created.json()
      const conversationId: string = conversation.id
      ```
    </CodeGroup>

    Response `201`:

    ```json theme={null}
    {
      "id": "9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93",
      "instance_id": "3b9d2f4e-8a1c-4e57-9f02-6c1d8e7a5b34",
      "end_user": "u_dana_ortiz",
      "title": "Getting started",
      "status": "active",
      "created_at": "2026-09-23T14:02:11.482913Z",
      "updated_at": "2026-09-23T14:02:11.482913Z"
    }
    ```
  </Step>

  <Step title="Send a message and read the stream">
    Post a turn with `message`. The response is a Server-Sent Events stream: each frame is an `event:` line, a `data:` line of JSON, and a blank line. Read frames until a `turn_end` or `error` frame arrives. Set no client read timeout shorter than the 10-minute server limit on a segment.

    <CodeGroup>
      ```bash curl theme={null}
      curl -N -X POST https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns \
        -H "Authorization: Bearer $NEXIO_API_KEY" \
        -H "Content-Type: application/json" \
        -H "Accept: text/event-stream" \
        -d '{
          "end_user": "u_dana_ortiz",
          "message": "What kinds of questions can you help me with?"
        }'
      ```

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

      def stream_turn(conversation_id: str, body: dict):
          """Yield (event, data) pairs until a terminal frame."""
          with requests.post(
              f"{API}/conversation-instances/workspace-assistant/conversations/{conversation_id}/turns",
              headers={**HEADERS, "Accept": "text/event-stream"},
              json=body,
              stream=True,
              timeout=(10, 660),
          ) as resp:
              if resp.status_code != 200:
                  # Errors before the first frame use the JSON error envelope.
                  raise RuntimeError(resp.json())
              event = None
              for line in resp.iter_lines(decode_unicode=True):
                  if line.startswith("event: "):
                      event = line[len("event: "):]
                  elif line.startswith("data: "):
                      data = json.loads(line[len("data: "):])
                      yield event, data
                      if event in ("turn_end", "error"):
                          return

      answer = []
      for event, data in stream_turn(conversation_id, {
          "end_user": "u_dana_ortiz",
          "message": "What kinds of questions can you help me with?",
      }):
          if event == "conversation":
              turn_id = data["turn_id"]
          elif event == "text_delta":
              answer.append(data["text"])
          elif event == "turn_end":
              print("stop_reason:", data["stop_reason"])
          elif event == "error":
              print("turn failed:", data["code"], data["message"])
      print("".join(answer))
      ```

      ```typescript TypeScript theme={null}
      async function* streamTurn(conversationId: string, body: Record<string, unknown>) {
        const resp = await fetch(
          `${API}/conversation-instances/workspace-assistant/conversations/${conversationId}/turns`,
          {
            method: "POST",
            headers: { ...HEADERS, "Content-Type": "application/json", Accept: "text/event-stream" },
            body: JSON.stringify(body),
          },
        )
        if (resp.status !== 200 || !resp.body) {
          // Errors before the first frame use the JSON error envelope.
          throw new Error(JSON.stringify(await resp.json()))
        }
        const reader = resp.body.getReader()
        const decoder = new TextDecoder()
        let buffer = ""
        while (true) {
          const { value, done } = await reader.read()
          if (done) return
          buffer += decoder.decode(value, { stream: true })
          let sep: number
          while ((sep = buffer.indexOf("\n\n")) !== -1) {
            const raw = buffer.slice(0, sep)
            buffer = buffer.slice(sep + 2)
            let event = ""
            let data = ""
            for (const line of raw.split("\n")) {
              if (line.startsWith("event: ")) event = line.slice(7)
              else if (line.startsWith("data: ")) data = line.slice(6)
            }
            const payload = JSON.parse(data)
            yield { event, payload }
            if (event === "turn_end" || event === "error") return
          }
        }
      }

      let answer = ""
      for await (const { event, payload } of streamTurn(conversationId, {
        end_user: "u_dana_ortiz",
        message: "What kinds of questions can you help me with?",
      })) {
        if (event === "text_delta") answer += payload.text
        if (event === "turn_end") console.log("stop_reason:", payload.stop_reason)
        if (event === "error") console.error("turn failed:", payload.code, payload.message)
      }
      console.log(answer)
      ```
    </CodeGroup>

    The stream for this message:

    ```text theme={null}
    event: conversation
    data: {"type":"conversation","conversation_id":"9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93","turn_id":"5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75","user_message_id":"c7d1e5a3-2f84-4b6c-9e0a-1b3d5f7a9c28"}

    event: text_delta
    data: {"type":"text_delta","text":"I can answer questions about this workspace: "}

    event: text_delta
    data: {"type":"text_delta","text":"its engines, recent runs and their status, and the data it can read."}

    event: turn_end
    data: {"type":"turn_end","stop_reason":"end_turn","usage":{"input_tokens":1840,"output_tokens":31,"total_tokens":1871,"cached_input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}
    ```

    The first frame is `conversation`. It carries the `turn_id` and the id of the user message the turn stored. `text_delta` frames carry the answer in pieces. The stream ends with exactly one `turn_end` or `error` frame. The answer text depends on the instance's system prompt and tools; the frame shapes do not. Every frame type is described on [Turns and streaming](/conversations/turns-and-streaming).

    If the connection drops, the turn still runs to completion on the server and is stored. Fetch the conversation to see the result.
  </Step>

  <Step title="Read the stored conversation">
    Fetch the conversation with the same `end_user`. The response holds the most recent 500 messages of the branch the conversation serves, oldest first.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93?end_user=u_dana_ortiz" \
        -H "Authorization: Bearer $NEXIO_API_KEY"
      ```

      ```python Python theme={null}
      resp = requests.get(
          f"{API}/conversation-instances/workspace-assistant/conversations/{conversation_id}",
          headers=HEADERS,
          params={"end_user": "u_dana_ortiz"},
      )
      resp.raise_for_status()
      for message in resp.json()["messages"]:
          print(message["role"], message["content"])
      ```

      ```typescript TypeScript theme={null}
      const detail = await fetch(
        `${API}/conversation-instances/workspace-assistant/conversations/${conversationId}?end_user=u_dana_ortiz`,
        { headers: HEADERS },
      ).then((r) => r.json())
      for (const message of detail.messages) console.log(message.role, message.content)
      ```
    </CodeGroup>

    Response `200`:

    ```json theme={null}
    {
      "conversation": {
        "id": "9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93",
        "instance_id": "3b9d2f4e-8a1c-4e57-9f02-6c1d8e7a5b34",
        "end_user": "u_dana_ortiz",
        "title": "Getting started",
        "status": "active",
        "created_at": "2026-09-23T14:02:11.482913Z",
        "updated_at": "2026-09-23T14:02:19.207551Z"
      },
      "messages": [
        {
          "id": "c7d1e5a3-2f84-4b6c-9e0a-1b3d5f7a9c28",
          "role": "user",
          "content": [{ "type": "text", "text": "What kinds of questions can you help me with?" }],
          "turn_id": "5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75",
          "config_version_hash": "4f1c9a7e2b8d3065",
          "created_at": "2026-09-23T14:02:13.118402Z",
          "parent_message_id": null
        },
        {
          "id": "e2b8f4c6-7a1d-4c3e-8f95-0d6a2c4e8b17",
          "role": "assistant",
          "content": [
            {
              "type": "text",
              "text": "I can answer questions about this workspace: its engines, recent runs and their status, and the data it can read."
            }
          ],
          "turn_id": "5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75",
          "config_version_hash": "4f1c9a7e2b8d3065",
          "created_at": "2026-09-23T14:02:19.203117Z",
          "parent_message_id": "c7d1e5a3-2f84-4b6c-9e0a-1b3d5f7a9c28"
        }
      ],
      "truncated": false
    }
    ```

    `config_version_hash` records which released config answered. `parent_message_id` links messages on their branch; see [Branching](/conversations/branching).
  </Step>
</Steps>

## What to build next

| Goal                                                           | Page                                                          |
| -------------------------------------------------------------- | ------------------------------------------------------------- |
| Handle every frame, stop reason, and mid-stream error          | [Turns and streaming](/conversations/turns-and-streaming)     |
| Let the assistant call your own code, with approval for writes | [Client tools and confirmations](/conversations/client-tools) |
| Attach PDFs, spreadsheets, emails, or folders to a message     | [Attachments](/conversations/attachments)                     |
| Let users revise a sent message                                | [Branching](/conversations/branching)                         |
| Collect ratings and turn them into regression tests            | [Evaluation](/conversations/evaluation)                       |
| Hand a transcript to a reviewer                                | [Export and retention](/conversations/export-and-retention)   |

## Common errors

| Status and code                                      | Cause                                                                                         | Fix                                                                                  |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `400 invalid_request` "end\_user is required."       | The turn body has no `end_user`.                                                              | Send `end_user` on every turn and every conversation request.                        |
| `400 invalid_request` "depth is no longer supported" | The body sends the retired `depth` field.                                                     | Remove `depth`. Every turn uses the platform's one model.                            |
| `403 insufficient_capability`                        | A scoped key without `conversations:use`.                                                     | Ask Nexio to add the capability, or use a key that has it.                           |
| `403 instance_archived`                              | The instance is archived.                                                                     | Reactivate it in the portal or with `PATCH` and `status: active`.                    |
| `404 instance_not_found`                             | The slug does not exist in your org.                                                          | Check the slug with the instance list.                                               |
| `404 conversation_not_found`                         | Wrong conversation id, or a different `end_user` or environment than the one that created it. | Use the same key environment and `end_user` that created the conversation.           |
| `409 instance_not_published`                         | The instance has no published version.                                                        | Publish a version first.                                                             |
| `409 turn_in_progress`                               | Another turn segment is still running on this conversation.                                   | Wait for the running stream to end, then send.                                       |
| `409 pending_turn`                                   | The last turn paused for a client tool or a confirmation.                                     | Resolve it first. See [Client tools and confirmations](/conversations/client-tools). |
| `503 converse_unavailable`                           | Conversations are not enabled in this deployment.                                             | Contact Nexio.                                                                       |
