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

# Take a turn

> Send a message, tool results, or confirmations and stream the turn as Server-Sent Events.

## Behavior

The response is a Server-Sent Events stream, not JSON. Each frame is `event: <type>`, then `data: <json>`, then a blank line, and every stream ends with exactly one `turn_end` or `error` frame. A failure before the first frame is a normal JSON error; after it, the status is already `200` and the failure arrives as an `error` frame.

`tool_results` and `confirmations` must cover exactly the call ids the paused turn is waiting for: no missing ids, no extras, no duplicates. A new `message` while a turn is paused is `409 pending_turn` with `details.pause_kind` and `details.pending_call_ids`.

The turn keeps running if the client disconnects. One segment may run for up to 10 minutes. Turn requests have their own rate limit bucket per org: 300 requests per minute, or the deployment's default limit when that is higher.

Every frame, the turn state model, and every stop reason are on [Turns and streaming](/conversations/turns-and-streaming). Resumes are on [Client tools and confirmations](/conversations/client-tools).


## OpenAPI

````yaml POST /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/turns
openapi: 3.1.0
info:
  title: Nexio API
  version: '1.0'
  description: |
    The Nexio public API. Submit runs to configured engines and read their
    results, hold conversations with configured assistants, make authorized
    reads over the planes and families of a connected system of record, and
    manage webhooks and environments.

    Six engine types are registered: comparison, matching, entity_analysis,
    diligence, triage, and opportunity.

    Every route under /api/v1/ requires `Authorization: Bearer <key>`, except
    inbound event ingest (`POST /api/v1/events/ingest/{source_key}`), which
    authenticates each request against its ingest source instead: an
    HMAC-SHA256 signature for `nexio` and `github` sources, or a shared
    authentication code for an `ams360_ons` source. Error
    bodies share the `Error` schema. Call the API from servers only: it sends
    no CORS headers.
  contact:
    email: support@usenexio.com
    url: https://docs.usenexio.com
servers:
  - url: https://api.usenexio.com
    description: >-
      Nexio API. Sandbox or live is chosen by the API key's environment, not by
      the host.
security:
  - BearerAuth: []
tags:
  - name: EngineManagement
    description: >-
      Create, configure, version and manage engines. An engine is a
      configuration of one registered engine type.
  - name: Engines
    description: Engine-scoped endpoints for submitting runs and retrieving results.
  - name: Runs
    description: Retrieve and reconcile submitted runs.
  - name: Records
    description: >-
      Typed, authorized reads over the registered planes and families of a
      connected system of record, and governed writes to the action ledger. The
      generic family read is the core contract.
  - name: Graph
    description: >-
      The organization's data graph: a read-only map of connections and the
      derivations scheduled on them. Node kinds are a closed registry of 16; the
      API serves 14.
  - name: Environments
    description: >-
      The live environment and up to 5 sandbox environments in your org.
      Organization keys only.
  - name: Webhooks
    description: >-
      Manage webhook endpoints that receive terminal run events (run.completed,
      run.failed, run.cancelled) and run corrections (run.superseded).
  - name: Events
    description: >-
      Send inbound events to your organization's event log through an ingest
      source. Source kinds are nexio, github and ams360_ons; a nexio source
      sends event types from your own vocabulary.
  - name: Converse
    description: The raw stateless conversational model turn (caller-owned state).
  - name: Conversations
    description: >-
      Conversation instances (orchestrators over engines) and their managed
      conversations, turns, annotations, evals, and exports.
  - name: Platform
    description: Service health. No key needed.
paths:
  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/turns:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Conversations
      summary: Take a Converse turn (streaming)
      description: >
        Runs one instance turn on the conversation and streams the result as

        Server-Sent Events. Exactly one of `message`, `tool_results`, or

        `confirmations` starts a turn: a new user message, the results of a

        client-tool handoff, or the decisions for a paused confirm gate.


        The instance's published config governs the turn: model policy,

        platform tool packs (executed server-side, filtered at turn start to

        what the calling principal may invoke), client tools (returned to the

        caller with a `tool_use` frame; the turn stays open for the follow-up

        `tool_results` POST), guardrail policy (refusals and escalations emit

        `guardrail` frames and are recorded), and per-turn limits. Every

        persisted message is stamped with the instance config version hash the

        turn resolved. A new `message` needs a published version

        (`instance_not_published`). The retired `depth` field is refused with

        `400 invalid_request`; every configured assistant runs `gpt-6-sol`.


        The response is `text/event-stream`. Frames: `conversation`,
        `attachment`,

        `text_delta`, `tool_activity`, `tool_use`, `component`, `guardrail`,

        `pending_confirmation`, `turn_end`, `error`. A failure before the

        first byte returns the standard JSON error envelope; a failure after

        the stream has started arrives as a terminal `error` event. The turn

        survives client disconnect: it runs to completion and persists, and

        the finished exchange is present when the conversation is fetched again.

        One segment is limited to 10 minutes on the server. The route is not

        subject to the 30 second request timeout.


        Turns are serialized per conversation: a second POST while a segment is

        running returns `409` (`turn_in_progress`), a new `message` while a

        paused turn awaits `tool_results` or `confirmations` returns `409`

        (`pending_turn`), a resume that does not match the paused state

        returns `409` (`turn_state_conflict`), and an archived conversation
        returns

        `409` (`conversation_archived`). A resume runs under the paused turn's

        original config version; only a new `message` resolves the latest

        release.


        A turn can also REVISE an earlier user message instead of appending to

        the transcript: send `edit_of_message_id` alongside `message`. The

        revision is stored as a sibling version of the referenced message, the

        superseded exchange is left out of the history the model sees, and the

        reply streams on the new branch, which the conversation serves from

        then on. An unparseable id, a missing `message`, or a target that is

        not a `user` message returns `400`; a target outside this org,

        conversation, or end user returns `404`. Switch between versions with

        the branch route.
      operationId: converseConversationTurn
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - end_user
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to; required on every turn. A missing assertion is
                    a 400, a mismatch a 404.
                message:
                  type: string
                  maxLength: 32000
                  description: |
                    New end-user input (at most 32000 characters; longer
                    messages reject with `message_too_long`). Leading and
                    trailing spaces are trimmed; a blank message answers
                    `invalid_turn_request`. Exactly one of the three starters.
                edit_of_message_id:
                  type: string
                  format: uuid
                  description: |
                    Revises the referenced user message: the value of `message`
                    is stored as a sibling version of that message, and the new
                    branch is served from then on. Requires `message`; omit it
                    on an ordinary turn.
                attachment_ids:
                  type: array
                  uniqueItems: true
                  items:
                    type: string
                    format: uuid
                  description: |
                    Attachments this message carries. Each must already be
                    uploaded to this conversation and belong to the same end
                    user; the platform loads the bytes and hands the files to
                    the model. Requires `message`, and is rejected alongside
                    `tool_results` or `confirmations`.

                    A named attachment that is missing, out of scope, or not
                    yet readable fails the whole turn rather than running it
                    with fewer documents than were attached. An attachment
                    that is a container (an email or a zip) resolves to the
                    files inside it. The combined bytes are capped at the
                    provider request limit; over it returns `413`.
                tool_results:
                  type: array
                  description: Client-tool results completing a `tool_use` handoff.
                  items:
                    type: object
                    required:
                      - tool_call_id
                    properties:
                      tool_call_id:
                        type: string
                        description: The handed-off call this result answers.
                      content:
                        description: >-
                          Opaque tool output passed to the model verbatim.
                          Absent content is sent as an empty string.
                      is_error:
                        type: boolean
                        description: True when the client tool failed.
                confirmations:
                  type: array
                  description: Decisions resolving a `pending_confirmation` pause.
                  items:
                    type: object
                    required:
                      - tool_call_id
                      - approved
                    properties:
                      tool_call_id:
                        type: string
                        description: The confirm-gated call this decision resolves.
                      approved:
                        type: boolean
                      reason:
                        type: string
                        description: Optional denial reason surfaced to the model.
                page_context:
                  description: |
                    Untrusted consumer page context, any JSON value, at most
                    16384 bytes. Sent to the model as a delimited data block on
                    every round of this segment; never stored.
      responses:
        '200':
          description: |
            A Server-Sent Events stream of the instance turn. Frames are
            `event: <type>` with a JSON `data:` payload.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE frames, each `event: <type>` then `data: <json>` then a
                  blank line. Every payload carries `type`.
                  `attachment` (`{type, message_id, attachments: [{id, name,
                  media_type, notice?}]}`) comes first, only when the message
                  carries files. `conversation` (`{type, conversation_id,
                  turn_id, user_message_id?}`) comes first otherwise;
                  `user_message_id` is absent on a resume. Then any of
                  `guardrail` (`{type, rule_id, decision}`, decision
                  `refused`, `escalated`, `output_check_triggered`),
                  `text_delta` (`{type, text}`), `tool_activity`
                  (`{type, tool_call_id, name, execution, phase}`, execution
                  `platform` or `client`, phase `started`, `completed`,
                  `failed`), `tool_use` (`{type, tool_call_id, name, input}`,
                  client tools only), `component` (`{type, component, version,
                  props}`), `pending_confirmation` (`{type, tool_call_id, name,
                  input, reason, execution}`). Exactly one terminal frame ends
                  the segment: `turn_end` (`{type, stop_reason, usage}`) or
                  `error` (`{type, code, message, reason?}`; codes include
                  `provider_error`, `provider_unavailable`,
                  `invalid_conversation_history`, `attachment_changed`,
                  `attachment_read_failed`, `attachments_unavailable` and
                  `internal_error`). `usage` is
                  cumulative across the turn's segments and has the six token
                  fields of the stateless converse route. `stop_reason` is a
                  provider value (`end_turn`, `max_tokens`) or one of
                  `tool_use`, `pending_confirmation`, `refusal`, `escalated`,
                  `output_check_triggered`, `max_output_tokens`.
        '400':
          description: |
            `invalid_request` (body not JSON or over 1 MiB, `end_user` missing
            or blank, or `depth` sent, even as null), `invalid_turn_request`
            (not exactly one starter, blank `message`, `edit_of_message_id`
            without `message` or not a UUID, `page_context` over 16384 bytes,
            `attachment_ids` without `message`, not UUIDs, or repeated),
            `message_too_long`, `invalid_tool_result` or
            `invalid_confirmation` (including results or decisions that do
            not cover exactly the pending call ids), `invalid_edit_target`,
            `attachments_not_enabled`, `too_many_attachments` (over the
            instance's per-message limit, or over 25 files once containers are
            opened), `attachment_not_accepted`, `missing_instance_slug` (the
            slug in the path is blank). The turn-request rules run after the
            conversation lookup, the archive check and, for a new `message`,
            the published-version check, so those 404 and 409 answers come
            first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            A scoped key lacks the capability (`insufficient_capability`), or
            the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            `instance_not_found` (unknown slug), `conversation_not_found`
            (unknown or malformed id, or a different org, environment,
            instance, or end user), `message_not_found`
            (`edit_of_message_id` is not a message in this conversation),
            `attachment_not_found` (a named attachment is missing or out of
            scope).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            `turn_in_progress` (another segment is running),
            `pending_turn` (a paused turn must be resumed first; `details`
            carries `pause_kind` `confirm` or `handoff` and
            `pending_call_ids`), `turn_state_conflict` (the resume does not
            match the paused state), `conversation_archived`,
            `instance_not_published` (a new message on an instance with no
            published version), `confirmation_environment_unpinned` (the
            paused action has no recorded environment). A named attachment
            removed while the turn ran (`attachment_changed`) arrives as a
            terminal `error` frame, not as this status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: >-
            The files on the message exceed 52428800 bytes once base64-encoded
            (`attachments_too_large`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: |
            The required `gpt-6-sol` model is not available
            (`instance_model_unavailable`; no fallback model is used), the
            guardrail classifier failed (`guardrail_evaluation_failed`), an
            output check does not compile (`guardrail_config_invalid`), or a
            platform fault (`instance_config_missing`,
            `instance_config_invalid`, `attachment_read_failed`,
            `streaming_unsupported`, `auth_context_missing`,
            `internal_error`). `invalid_conversation_history` is raised only
            after the stream starts, so it arrives as a terminal `error`
            frame.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >
            Converse is not enabled in this deployment

            (`converse_unavailable`), or attachment storage is not configured

            (`attachments_unavailable`). A model provider failure

            (`provider_error`, `provider_unavailable`) happens after the first

            frame, so it arrives as a terminal `error` frame on a `200` stream,

            never as an HTTP status.


            Also `auth_unavailable`: API key authentication was briefly
            unavailable before the route ran. That cause is transient; retry it
            with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    InstanceSlug:
      name: instance_slug
      in: path
      required: true
      description: Conversation instance identifier slug (e.g. `platform-assistant`).
      schema:
        type: string
  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: >
            Stable snake_case error identifier. Safe to match programmatically.

            New codes are added over time; treat an unknown code by its HTTP
            status.


            Known codes include:

            `invalid_request`, `invalid_input`, `unauthorized`,

            `auth_unavailable`, `rate_limited`, `missing_run_id`,

            `invalid_run_id`, `run_not_found`, `invalid_offerings`,

            `missing_input`, `queue_unreachable`, `engine_not_found`,

            `engine_slug_conflict`, `engine_archived`, `instance_not_found`,

            `instance_slug_conflict`, `instance_archived`,

            `instance_follows_canonical`, `instance_not_published`,

            `invalid_engine_type`, `validation_error`, `webhook_not_found`,

            `webhook_limit_exceeded`, `invalid_url`, `invalid_events`,

            `invalid_description`, `invalid_auth_token`,

            `missing_endpoint_id`, `invalid_endpoint_id`,

            `missing_delivery_id`, `invalid_delivery_id`,

            `delivery_not_found`, `delivery_not_resendable`,

            `insufficient_capability`, `engine_version_required`,

            `engine_version_exact_required`, `engine_version_not_found`,

            `engine_version_invalid_format`,

            `engine_version_draft_requires_sandbox_key`,

            `engine_version_none_released`, `test_scenario_sandbox_only`,

            `test_scenario_forbidden`, `test_scenario_exact_version_required`,

            `test_scenario_version_not_supported`, `invalid_test_scenario`,

            `request_bound_exceeded`, `run_cap_exceeded`,

            `scoped_key_required`, `engine_binding_forbidden`,

            `request_timeout`, `internal_error`, `idempotency_key_reused`,

            `invalid_idempotency_key`, `acting_principal_mismatch`,

            `run_requires_acting_principal`, `environment_slug_invalid`,

            `environment_slug_reserved`, `environment_slug_taken`,

            `environment_name_invalid`, `environment_limit_reached`,

            `environment_live_immutable`, `environment_not_found`,

            `environment_in_use`, `environment_operation_failed`,

            `list_environments_failed`, `engine_release_unservable`,

            `engine_config_hash_inconsistent`,

            `engine_config_hash_unavailable`,

            `engine_config_version_unavailable`,

            `engine_version_resolve_failed`,

            `engine_config_version_load_failed`,

            `engine_version_publish_mismatch`,

            `engine_version_schema_change_requires_major`,

            `engine_config_hash_missing`, `engine_config_changed`,

            `invalid_engine_config`, `cold_start_gate_not_met`,

            `cold_start_gate_failed`, `cold_start_gate_regressed`,

            `cancel_run_failed`, `event_id_reused`, `invalid_event_id`,

            `missing_event_id`, `invalid_event_type`, `invalid_payload`,

            `reason_text_too_long`, `reason_taxonomy_version_unknown`,

            `invalid_rating`, `missing_comment`, `invalid_target`,

            `invalid_time_on_task`, `scoped_annotation_required`,

            `missing_instance_slug`, `invalid_instance_config`,

            `invalid_eval_waiver`, `instance_config_hash_missing`,

            `config_changed_during_publish`,

            `scenario_set_changed_during_publish`,

            `conversation_eval_regressed`,

            `conversation_eval_execution_failed`,

            `conversation_eval_gate_unavailable`, `publish_failed`,

            `conversation_not_found`, `conversation_archived`,

            `invalid_cursor`, `message_not_found`, `invalid_turn_request`,

            `message_too_long`, `invalid_tool_result`, `invalid_confirmation`,

            `invalid_edit_target`, `turn_in_progress`, `pending_turn`,

            `turn_state_conflict`, `confirmation_environment_unpinned`,

            `instance_config_missing`, `instance_config_invalid`,

            `instance_model_unavailable`, `guardrail_evaluation_failed`,

            `guardrail_config_invalid`, `invalid_conversation_history`,

            `streaming_unsupported`, `converse_unavailable`, `provider_error`,

            `provider_unavailable`, `unknown_model`, `too_many_messages`,

            `too_many_tools`, `invalid_max_tokens`, `invalid_message_role`,

            `invalid_message`, `invalid_content_block`, `invalid_tool`,

            `attachments_not_enabled`, `attachments_unavailable`,

            `attachment_rejected`, `attachment_too_large`,

            `attachments_too_large`, `too_many_attachments`,

            `attachment_not_accepted`, `attachment_not_found`,

            `attachment_changed`, `attachment_read_failed`,

            `attachment_member_delete`, `attachment_not_reservable`,

            `invalid_offset`, `invalid_turn_id`, `invalid_comment`,

            `invalid_reason`, `invalid_feedback_key`, `turn_not_found`,

            `annotation_not_found`, `annotation_not_promotable`,

            `invalid_eval_scenario`, `conversation_eval_scenario_not_found`,

            `conversation_eval_scenario_cap`,

            `conversation_eval_run_not_found`,

            `conversation_eval_baseline_not_found`,

            `conversation_eval_unavailable`,

            `instance_version_invalid_format`, `instance_version_not_found`,

            `document_class_not_open`, `presign_failed`,

            `markets_directory_unavailable`, `book_unavailable`,

            `book_connection_ambiguous`, `identity_unmapped`,

            `identity_needs_review`, `identity_suspended`, `identity_stale`,

            `scope_unavailable`, `assertion_invalid`, `assertion_stale`,

            `cursor_filter_mismatch`, `cursor_expired`,

            `action_schema_unknown`, `action_payload_invalid`,

            `action_out_of_scope`, `action_list_too_large`,

            `overlay_read_only`, `unsupported_node_type`,

            `acting_principal_required`, `action_denied`,

            `appetite_read_denied`, `approval_required`,

            `catalog_access_denied`, `catalog_connection_ambiguous`,

            `create_webhook_failed`, `dataset_denied`, `delivery_id_reused`,

            `document_type_not_servable`, `document_unclassified`,

            `egress_manifest_version_mismatch`,

            `egress_manifest_version_required`, `event_type_not_allowed`,

            `event_type_reserved`, `execution_confirm_required`,

            `execution_prohibited`, `ingest_source_disabled`,

            `ingest_source_misconfigured`, `ingest_source_not_found`,

            `invalid_acting_assertion`, `invalid_active`,

            `invalid_credentials`, `invalid_payload_mode`,

            `invalid_signature`, `load_run_failed`, `load_solutions_failed`,

            `load_work_items_failed`, `missing_engine_slug`, `not_found`,

            `object_store_unconfigured`, `provider_not_approved`,

            `resolve_retry_exhausted`, `run_lookup_failed`,

            `service_identity_unknown`, `stale_timestamp`, `surface_denied`,

            `unresolved_market_question`, `unsupported_market_filter`.


            The full list with causes and fixes is at

            https://docs.usenexio.com/reference/errors.
        message:
          type: string
          description: Human-readable error message. May change between versions.
        details:
          description: |
            Optional request-specific details. Request-bound failures use the
            `RequestBoundDetails` object. Validation failures may use an array
            of field issues or another documented object.

            `409 environment_in_use` is the one exception to this envelope: it
            carries a top-level `blockers` object (see `EnvironmentInUseError`)
            instead of `details`.
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthorized
            message: Missing or invalid API key
    RateLimited:
      description: Rate limit exceeded. Retry after the window resets.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limited
            message: Rate limit exceeded
  headers:
    RetryAfter:
      description: Whole seconds to wait before retrying, at least 1.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >
        Send the key as `Authorization: Bearer <key>`. Two kinds of key exist.


        Organization keys are issued in the portal (Settings, then API keys),

        each bound to one environment, shaped `nx_<environment slug>_<64 hex>`.
        They carry

        no capabilities and pass every capability check, with one exception:

        routes under /api/v1/records, /api/v1/engines/{id}/opportunities,

        /api/v1/graph and /api/v1/catalog/documents accept an

        organization key only when its environment is `live`, and refuse any

        other with 403 `scoped_key_required`. Revocation takes effect within 60

        seconds.


        Scoped keys are issued by Nexio on request, shaped

        `nxsk_v1_<24 hex key id>_<43 character secret>`. Each is bound to one

        org, one environment, a set of engines and a set of capabilities. A

        malformed, unknown or revoked `nxsk_` key fails with 401 and is never

        retried as an organization key. Revocation takes effect on the next

        request. A scoped key without a route's capability gets 403

        `insufficient_capability`; a scoped key not bound to the engine gets 403

        `engine_binding_forbidden`.


        Key-grantable capabilities: `engines:read`, `runs:write`, `runs:read`,

        `runs:defensibility:read`, `runs:test`, `catalog:read`,

        `catalog:documents:read`, `webhooks:manage`, `conversations:use`,

        `conversations:export`, `records:read`, `records:opportunities:run`,

        `actions:write`, `actions:read`, `graph:read`, `records:analyze`.


        Routes that accept organization keys only (every scoped key gets 403

        `insufficient_capability`): environment management, engine create,

        update, configuration and publish, and conversation instance

        authoring. Each operation description names the capability a scoped

        key needs.

````